vxlan 0.2.1 → 0.2.3
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 +27 -0
- data/README-zh.md +8 -0
- data/README.md +8 -0
- data/docs/architecture-zh.md +1 -1
- data/lib/vxlan/bgp_neighbors.rb +4 -1
- data/lib/vxlan/cisco_nxos/configuration.rb +3 -77
- data/lib/vxlan/cisco_nxos/configuration_state.rb +87 -0
- data/lib/vxlan/cisco_nxos/document.rb +1 -1
- data/lib/vxlan/cisco_nxos/evpn_l2route.rb +32 -23
- data/lib/vxlan/cisco_nxos/parser.rb +9 -9
- data/lib/vxlan/cisco_nxos/parsers/show_bgp_l2vpn_evpn_summary.rb +7 -7
- data/lib/vxlan/cisco_nxos/parsers/show_cdp_neighbors_detail.rb +7 -7
- data/lib/vxlan/cisco_nxos/parsers/show_interface_brief.rb +8 -8
- data/lib/vxlan/cisco_nxos/parsers/show_ip_ospf_neighbors.rb +4 -4
- data/lib/vxlan/cisco_nxos/parsers/show_mac_address_table.rb +4 -4
- data/lib/vxlan/cisco_nxos/parsers/show_nve_peers.rb +4 -4
- data/lib/vxlan/cisco_nxos/parsers/show_nve_vni.rb +3 -3
- data/lib/vxlan/cisco_nxos/parsers/show_port_channel_summary.rb +7 -7
- data/lib/vxlan/cisco_nxos/parsers/show_running_config.rb +74 -37
- data/lib/vxlan/cisco_nxos/parsers/show_vpc_brief.rb +10 -10
- data/lib/vxlan/cisco_nxos/syntax/interfaces.rb +3 -3
- data/lib/vxlan/cisco_nxos/syntax/policies.rb +35 -13
- data/lib/vxlan/cisco_nxos/syntax/routing.rb +45 -6
- data/lib/vxlan/cisco_nxos/terminal.rb +3 -1
- data/lib/vxlan/collector.rb +4 -4
- data/lib/vxlan/configuration_review.rb +13 -2
- data/lib/vxlan/prefix_list_references.rb +3 -3
- data/lib/vxlan/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 715f96e8f586f8a7bacd62d8a7d5e47d4665884cdc20c3953d75c6282184955d
|
|
4
|
+
data.tar.gz: c3e70fb64cb7677d3e960d05f11622720f0a5842fc0171c7775047d0fd6b8d8a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c13eb74ea3ec0e75a8985a3bdf92503db849a6fc55e90af1a7a8ce051eaa810cdf50b25ab90869dae9696ca85bec142a83f0b7bbd649c5e6a7c6aea4019a09b6
|
|
7
|
+
data.tar.gz: 2bbb1204ab00cfcc50998a878b3d61c8614b03cc158833fecb1c3f1a2835672751bdd89d36e9dd79eca3cd893f91dfb4870c5ff8499817a2fe53bc758ec5eee4
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
Changes to the public contract are recorded here. Versions follow Semantic Versioning.
|
|
4
4
|
|
|
5
|
+
## 0.2.3 (2026-10-18)
|
|
6
|
+
|
|
7
|
+
- Validate optional IPv4 next hops following static-route output interfaces without changing route fields or option text.
|
|
8
|
+
|
|
9
|
+
- Clear Bundler bootstrap variables when verifying packages in an isolated gem installation, including calls made through bundle exec.
|
|
10
|
+
|
|
11
|
+
- Reject unsupported or misplaced VRF address families before projecting targets and policy consumers, and validate anycast gateway MAC values and static-route next-hop forms.
|
|
12
|
+
|
|
13
|
+
- Parse runtime line boundaries and wrapped records independently of the host Ruby input/output record separators.
|
|
14
|
+
|
|
15
|
+
## 0.2.2 (2026-10-18)
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Name mutable NX-OS parsing state `CiscoNxos::ConfigurationState`; retain the former `Configuration` constant, require path, and `finish` method as compatible aliases.
|
|
20
|
+
- Clarify internal method names for conflict exclusion, diagnostics, address conversion, route-map consumers, and observation collection without changing parsed record fields.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- Exclude repeated prefix-list and route-map sequence identities, including malformed payloads, without restoring them later; preserve independent entries and capture their final array paths.
|
|
25
|
+
- Validate routing leaf ownership, supported address families, network prefix families, and IPv4 router IDs before accepting facts or projecting policy consumers.
|
|
26
|
+
- Exclude repeated hostname, BGP process, NVE interface, and vPC domain identities without restoring them on later declarations; invalidate their original source captures.
|
|
27
|
+
- Stop configured neighbor and policy projections through unsupported peer-template inheritance chains.
|
|
28
|
+
- Reject bare carriage returns in runtime output while preserving CRLF and existing configuration newline handling.
|
|
29
|
+
- Scan inline EVPN next hops by byte offset to avoid repeated copies of the remaining line.
|
|
30
|
+
- Report explicit EVPN/NVE layer mismatches and L3 NVE members without a matching configured VRF, independently of syntax completeness; retain valid VLAN-backed L3 VNIs.
|
|
31
|
+
|
|
5
32
|
## 0.2.1 (2026-10-17)
|
|
6
33
|
|
|
7
34
|
- Require ctx_parser and ctx_driver 0.2.x so configuration APIs and command capture behavior resolve consistently.
|
data/README-zh.md
CHANGED
|
@@ -72,10 +72,18 @@ observation.records.each { |record| p record }
|
|
|
72
72
|
|
|
73
73
|
VLAN ID、VNI、VRF、NVE 成员是不同身份。配置审查通过 VNI 关联 VLAN 映射与 NVE 成员,不推断运行可达性。`BgpNeighbors` 枚举应用 peer-template 继承后的配置邻居,不代表已建立的 BGP 会话。`PrefixListReferences` 表达 route-map 选择器及路由使用方的引用关系,引用本身不能证明路由被接受。
|
|
74
74
|
|
|
75
|
+
关联诊断包括:显式 EVPN 或 VRF 定义与 NVE 成员二层/三层角色矛盾时的 `vni_layer_conflict`,以及三层 NVE 成员没有对应 VNI 的配置 VRF 时的 `nve_l3_without_vrf`。VLAN 映射本身不能证明二层角色,支持通过 VLAN 承载的三层 VNI。这些诊断仅基于完整配置证据生成,不改变语法解析状态。
|
|
76
|
+
|
|
77
|
+
路由事实保留协议及所属作用域。`network` 必须位于匹配的 IPv4/IPv6 进程或 VRF 地址族中;邻居策略映射属于邻居或 peer-template 的地址族。不支持的路由地址族及位置错误的设置保持部分证据,不生成对应策略使用方。
|
|
78
|
+
|
|
79
|
+
prefix-list 或 route-map 在同种类、同名称内出现重复序号时,排除该序号的全部声明,后续声明不能恢复。独立序号仍作为部分事实保留,来源捕获路径使用最终结果索引;部分策略条目不能证明整份策略的行为。
|
|
80
|
+
|
|
75
81
|
## 开发与测试
|
|
76
82
|
|
|
77
83
|
共享类按实际职责命名,平台信息放在命名空间中。直接对应设备命令的解析器保留文件名和类名。
|
|
78
84
|
|
|
85
|
+
`CiscoNxos::ConfigurationState` 持有单次解析期间的可变事实,`Vxlan::Configuration` 是公开解析入口。原 `CiscoNxos::Configuration` 常量及其 `finish` 方法保留为兼容别名。内部方法名区分文本解析(`parse_next_hops`)、诊断记录(`unparsed_records`)和冲突排除(`exclude_ambiguous_records!`,移除重复身份的全部记录)。
|
|
86
|
+
|
|
79
87
|
```sh
|
|
80
88
|
bundle install
|
|
81
89
|
bundle exec rake
|
data/README.md
CHANGED
|
@@ -72,10 +72,18 @@ The lifecycle connector is a host adapter exposing `open { |session| }` (and `pl
|
|
|
72
72
|
|
|
73
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
74
|
|
|
75
|
+
Association findings include `vni_layer_conflict` for explicit EVPN or VRF definitions that disagree with the NVE member's L2/L3 role, and `nve_l3_without_vrf` for an L3 NVE member without a configured VRF using its VNI. A VLAN mapping alone does not establish an L2 role: VLAN-backed L3 VNIs are supported. These findings are emitted only from complete configuration evidence and do not change the syntax status.
|
|
76
|
+
|
|
77
|
+
Routing facts retain their protocol and owner scope. Network statements require a matching IPv4/IPv6 process or VRF address family; neighbor policy maps belong to neighbor or peer-template address families. Unsupported routing families and misplaced settings remain partial evidence and produce no corresponding policy consumer.
|
|
78
|
+
|
|
79
|
+
Repeated prefix-list or route-map sequence identities are excluded within their policy name and kind, including subsequent declarations of that sequence. Independent sequences remain available as partial facts; their source capture paths use the final result indices. These partial policy entries do not establish the complete policy's behavior.
|
|
80
|
+
|
|
75
81
|
## Development and tests
|
|
76
82
|
|
|
77
83
|
Name shared classes by their responsibility and use the namespace for platform context. Preserve filenames and class names that directly match device commands.
|
|
78
84
|
|
|
85
|
+
`CiscoNxos::ConfigurationState` holds mutable facts during a single parse; `Vxlan::Configuration` is the public parsing entry point. The former `CiscoNxos::Configuration` constant and its `finish` method remain compatible aliases. Internal method names distinguish parsing (`parse_next_hops`), diagnostics (`unparsed_records`), and conflict exclusion (`exclude_ambiguous_records!`, which removes every record with a repeated identity).
|
|
86
|
+
|
|
79
87
|
```sh
|
|
80
88
|
bundle install
|
|
81
89
|
bundle exec rake
|
data/docs/architecture-zh.md
CHANGED
|
@@ -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` 编排命令解析,`Syntax::Interfaces/Routing/Policies` 分别解释接口/NVE、路由上下文及策略语法,`CtxParser::Configuration` 管理逐行状态、嵌套来源路径与捕获,`CiscoNxos::Configuration`
|
|
52
|
+
配置解析按职责组合:`ShowRunningConfig` 编排命令解析,`Syntax::Interfaces/Routing/Policies` 分别解释接口/NVE、路由上下文及策略语法,`CtxParser::Configuration` 管理逐行状态、嵌套来源路径与捕获,`CiscoNxos::ConfigurationState` 持有单次解析状态(旧名 `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
|
|
data/lib/vxlan/bgp_neighbors.rb
CHANGED
|
@@ -10,7 +10,7 @@ module Vxlan
|
|
|
10
10
|
@bgp = bgp
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
#
|
|
13
|
+
# 合并受支持的对等体模板后,逐个返回配置邻居,不推断运行会话状态。
|
|
14
14
|
def each
|
|
15
15
|
return enum_for(:each) unless block_given?
|
|
16
16
|
|
|
@@ -22,6 +22,9 @@ module Vxlan
|
|
|
22
22
|
next if inherited_name && !peer_templates.key?(inherited_name)
|
|
23
23
|
|
|
24
24
|
peer_template = peer_templates.fetch(inherited_name, {})
|
|
25
|
+
# 模板自身继承尚不受支持,不能把部分模板当作已解析配置。
|
|
26
|
+
next if peer_template[:inherit_peer]
|
|
27
|
+
|
|
25
28
|
address_families = peer_template.fetch(:address_families, {}).merge(neighbor_configuration.fetch(:address_families, {})) do |_name, template_family, neighbor_family|
|
|
26
29
|
template_family.merge(neighbor_family.compact).merge(route_maps: template_family.fetch(:route_maps, {}).merge(neighbor_family.fetch(:route_maps, {})))
|
|
27
30
|
end
|
|
@@ -1,84 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require_relative "configuration_state"
|
|
4
4
|
|
|
5
5
|
module Vxlan
|
|
6
6
|
module CiscoNxos
|
|
7
|
-
#
|
|
8
|
-
|
|
9
|
-
attr_reader :data, :issues
|
|
10
|
-
|
|
11
|
-
# 初始化单次配置解析使用的可变 NX-OS 事实容器。
|
|
12
|
-
def initialize
|
|
13
|
-
@data = { hostname: nil, features: [], disabled_features: [], breakouts: {}, interfaces: {}, vlan_vnis: {}, vrfs: {}, nve: { members: {} },
|
|
14
|
-
evpn: {}, bgp: {}, ospf: {}, vpc_domain: nil, unnumbered_addresses: {}, prefix_lists: {}, ipv6_prefix_lists: {}, route_maps: {}, acls: {}, static_routes: [] }
|
|
15
|
-
@issues = []
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
# 解析结束后关联跨块引用,避免声明顺序影响结果。
|
|
19
|
-
def finish
|
|
20
|
-
validate_templates
|
|
21
|
-
resolve_unnumbered
|
|
22
|
-
issues << { code: :conflicting_feature_state } if (data[:features] & data[:disabled_features]).any?
|
|
23
|
-
issues << { code: :hostname_missing } unless data[:hostname]
|
|
24
|
-
data[:features] = data[:features].uniq.sort
|
|
25
|
-
data[:disabled_features] = data[:disabled_features].uniq.sort
|
|
26
|
-
data[:segments] = segments
|
|
27
|
-
data
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# 展示对象身份与状态,避免泄露原始回显或凭据。
|
|
31
|
-
def inspect = "#<#{self.class} interfaces=#{data[:interfaces].size} issues=#{issues.size}>"
|
|
32
|
-
|
|
33
|
-
private
|
|
34
|
-
|
|
35
|
-
# 借用的 IPv4 地址仅作为主机身份,不猜测链路网段。
|
|
36
|
-
def resolve_unnumbered
|
|
37
|
-
interfaces = data[:interfaces]
|
|
38
|
-
by_name = interfaces.group_by { |name, _values| name.downcase }
|
|
39
|
-
interfaces.each do |name, values|
|
|
40
|
-
next unless values[:unnumbered_interface]
|
|
41
|
-
|
|
42
|
-
sources = by_name.fetch(values[:unnumbered_interface].downcase, [])
|
|
43
|
-
source = sources.one? ? sources.first.last : nil
|
|
44
|
-
if source && source[:addresses].one? && values[:addresses].empty? &&
|
|
45
|
-
!source[:unnumbered_interface] && source.fetch(:vrf, "default") == values.fetch(:vrf, "default")
|
|
46
|
-
data[:unnumbered_addresses][name] = [ "#{source[:addresses].first.split('/').first}/32" ]
|
|
47
|
-
else
|
|
48
|
-
issues << { code: :unresolved_unnumbered_interface, interface: name }
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# 报告未解析的继承模板,不虚构模板配置。
|
|
54
|
-
def validate_templates
|
|
55
|
-
bgp = data[:bgp]
|
|
56
|
-
templates = bgp.fetch(:templates, {})
|
|
57
|
-
issues << { code: :unsupported_peer_inheritance } if templates.values.any? { |settings| settings[:inherit_peer] }
|
|
58
|
-
[ bgp, *bgp.fetch(:vrfs, {}).values ].each do |scope|
|
|
59
|
-
scope.fetch(:neighbors, {}).each_value do |neighbor|
|
|
60
|
-
next unless neighbor[:inherit_peer] && !templates.key?(neighbor[:inherit_peer])
|
|
61
|
-
|
|
62
|
-
issues << { code: :missing_peer_template }
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
# 根据显式配置构建 VLAN 与 VNI 网段关联。
|
|
68
|
-
def segments
|
|
69
|
-
data[:interfaces].filter_map do |name, values|
|
|
70
|
-
match = /\AVlan(\d+)\z/.match(name)
|
|
71
|
-
next unless match && data[:vlan_vnis][match[1].to_i] && values[:addresses].any?
|
|
72
|
-
unless values[:addresses].one? && values[:vrf]
|
|
73
|
-
issues << { code: :unsupported_svi, interface: name }
|
|
74
|
-
next
|
|
75
|
-
end
|
|
76
|
-
source = values[:addresses].first
|
|
77
|
-
{ vlan_id: match[1].to_i, vni: data[:vlan_vnis].fetch(match[1].to_i), vrf: values[:vrf],
|
|
78
|
-
prefix: CtxParser::Values.ipv4_prefix(source), gateway: source.split("/").first,
|
|
79
|
-
anycast: values[:anycast], acl_bindings: values[:acl_bindings] }
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
end
|
|
7
|
+
# 兼容既有加载路径;配置解析状态由 ConfigurationState 表达。
|
|
8
|
+
Configuration = ConfigurationState
|
|
83
9
|
end
|
|
84
10
|
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ctx_parser/values"
|
|
4
|
+
|
|
5
|
+
module Vxlan
|
|
6
|
+
module CiscoNxos
|
|
7
|
+
# 归纳 NX-OS 模板引用、功能身份及 SVI、VLAN、VNI 子网关系。
|
|
8
|
+
class ConfigurationState
|
|
9
|
+
attr_reader :data, :issues
|
|
10
|
+
|
|
11
|
+
# 初始化单次配置解析使用的可变 NX-OS 事实容器。
|
|
12
|
+
def initialize
|
|
13
|
+
@data = { hostname: nil, features: [], disabled_features: [], breakouts: {}, interfaces: {}, vlan_vnis: {}, vrfs: {}, nve: { members: {} },
|
|
14
|
+
evpn: {}, bgp: {}, ospf: {}, vpc_domain: nil, unnumbered_addresses: {}, prefix_lists: {}, ipv6_prefix_lists: {}, route_maps: {}, acls: {}, static_routes: [] }
|
|
15
|
+
@issues = []
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# 解析结束后关联跨块引用,避免声明顺序影响结果。
|
|
19
|
+
def finalize_facts
|
|
20
|
+
peer_template_issues
|
|
21
|
+
resolve_unnumbered_addresses
|
|
22
|
+
issues << { code: :conflicting_feature_state } if (data[:features] & data[:disabled_features]).any?
|
|
23
|
+
issues << { code: :hostname_missing } unless data[:hostname]
|
|
24
|
+
data[:features] = data[:features].uniq.sort
|
|
25
|
+
data[:disabled_features] = data[:disabled_features].uniq.sort
|
|
26
|
+
data[:segments] = build_svi_segments
|
|
27
|
+
data
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# 兼容原有解析状态的收尾入口。
|
|
31
|
+
alias finish finalize_facts
|
|
32
|
+
|
|
33
|
+
# 展示对象身份与状态,避免泄露原始回显或凭据。
|
|
34
|
+
def inspect = "#<#{self.class} interfaces=#{data[:interfaces].size} issues=#{issues.size}>"
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
# 借用的 IPv4 地址仅作为主机身份,不猜测链路网段。
|
|
39
|
+
def resolve_unnumbered_addresses
|
|
40
|
+
interfaces = data[:interfaces]
|
|
41
|
+
by_name = interfaces.group_by { |name, _values| name.downcase }
|
|
42
|
+
interfaces.each do |name, values|
|
|
43
|
+
next unless values[:unnumbered_interface]
|
|
44
|
+
|
|
45
|
+
sources = by_name.fetch(values[:unnumbered_interface].downcase, [])
|
|
46
|
+
source = sources.one? ? sources.first.last : nil
|
|
47
|
+
if source && source[:addresses].one? && values[:addresses].empty? &&
|
|
48
|
+
!source[:unnumbered_interface] && source.fetch(:vrf, "default") == values.fetch(:vrf, "default")
|
|
49
|
+
data[:unnumbered_addresses][name] = [ "#{source[:addresses].first.split('/').first}/32" ]
|
|
50
|
+
else
|
|
51
|
+
issues << { code: :unresolved_unnumbered_interface, interface: name }
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# 报告未解析的继承模板,不虚构模板配置。
|
|
57
|
+
def peer_template_issues
|
|
58
|
+
bgp = data[:bgp]
|
|
59
|
+
templates = bgp.fetch(:templates, {})
|
|
60
|
+
issues << { code: :unsupported_peer_inheritance } if templates.values.any? { |settings| settings[:inherit_peer] }
|
|
61
|
+
[ bgp, *bgp.fetch(:vrfs, {}).values ].each do |scope|
|
|
62
|
+
scope.fetch(:neighbors, {}).each_value do |neighbor|
|
|
63
|
+
next unless neighbor[:inherit_peer] && !templates.key?(neighbor[:inherit_peer])
|
|
64
|
+
|
|
65
|
+
issues << { code: :missing_peer_template }
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# 根据显式配置构建 VLAN 与 VNI 网段关联。
|
|
71
|
+
def build_svi_segments
|
|
72
|
+
data[:interfaces].filter_map do |name, values|
|
|
73
|
+
match = /\AVlan(\d+)\z/.match(name)
|
|
74
|
+
next unless match && data[:vlan_vnis][match[1].to_i] && values[:addresses].any?
|
|
75
|
+
unless values[:addresses].one? && values[:vrf]
|
|
76
|
+
issues << { code: :unsupported_svi, interface: name }
|
|
77
|
+
next
|
|
78
|
+
end
|
|
79
|
+
source = values[:addresses].first
|
|
80
|
+
{ vlan_id: match[1].to_i, vni: data[:vlan_vnis].fetch(match[1].to_i), vrf: values[:vrf],
|
|
81
|
+
prefix: CtxParser::Values.ipv4_prefix(source), gateway: source.split("/").first,
|
|
82
|
+
anycast: values[:anycast], acl_bindings: values[:acl_bindings] }
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -15,7 +15,7 @@ module Vxlan
|
|
|
15
15
|
@banner ||= CtxParser::Banner.new
|
|
16
16
|
return :ignored if @banner.consume(line.text)
|
|
17
17
|
|
|
18
|
-
if Terminal.invalid_output?(line.raw)
|
|
18
|
+
if Terminal.invalid_output?(line.raw, allow_cr_newlines: true)
|
|
19
19
|
issues << { code: :incomplete_configuration, line: line.number }
|
|
20
20
|
return :ignored
|
|
21
21
|
end
|
|
@@ -11,6 +11,9 @@ module Vxlan
|
|
|
11
11
|
"host ip" => :host_ip, "prod" => :producer, "flags" => :flags, "seq no" => :sequence,
|
|
12
12
|
"next-hops" => :next_hops, "next hop (s)" => :next_hops, "next hop(s)" => :next_hops }.freeze
|
|
13
13
|
HEADER = /Topology ID|Topology|Mac Address|Host IP|Prod|Flags|Seq No|Next-Hops|Next Hop \(s\)|Next Hop\(s\)/i
|
|
14
|
+
NEXT_HOP = /\G(?<hop>Ifindex\s+\d+|Local|(?:Eth(?:ernet)?|Po(?:rt-channel)?|nve|Lo(?:opback)?|Vlan|vPC)[\d\/.:-]+|[\da-f:.]+)(?:\s*\(Label:\s*(?<label>\d+)\))?(?=\s|,|\z)/i
|
|
15
|
+
NEXT_HOP_SEPARATOR = /\G(?:\s*,\s*|\s+)/
|
|
16
|
+
private_constant :NEXT_HOP, :NEXT_HOP_SEPARATOR
|
|
14
17
|
|
|
15
18
|
private
|
|
16
19
|
|
|
@@ -19,7 +22,7 @@ module Vxlan
|
|
|
19
22
|
type = self.class::TYPE
|
|
20
23
|
@totals = []
|
|
21
24
|
fields = previous = next_hop_index = nil
|
|
22
|
-
text.each_line.with_index(1) do |source, number|
|
|
25
|
+
text.each_line("\n").with_index(1) do |source, number|
|
|
23
26
|
line = source.strip
|
|
24
27
|
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/)
|
|
25
28
|
if line.match?(/\ATopology\b/i)
|
|
@@ -44,11 +47,11 @@ module Vxlan
|
|
|
44
47
|
if fields && line.match?(/\A\d+\s/)
|
|
45
48
|
values = line.split(/\s+/, fields.size)
|
|
46
49
|
raise ArgumentError unless values.size == fields.size
|
|
47
|
-
previous =
|
|
50
|
+
previous = parse_route_fields(fields.zip(values).to_h, type)
|
|
48
51
|
next_hop_index = nil
|
|
49
52
|
@rows << previous
|
|
50
53
|
elsif previous && source.match?(/\A\s+/)
|
|
51
|
-
hops =
|
|
54
|
+
hops = parse_next_hops(line)
|
|
52
55
|
# 首条续行建立索引,后续仅追加新下一跳;非法续行不会改动已有事实。
|
|
53
56
|
unless next_hop_index
|
|
54
57
|
previous[:next_hops].uniq!
|
|
@@ -73,8 +76,8 @@ module Vxlan
|
|
|
73
76
|
|
|
74
77
|
# 汇总已解析记录,并报告未解决或不完整的来源状态。
|
|
75
78
|
def finish(complete)
|
|
76
|
-
@totals.each { |count|
|
|
77
|
-
|
|
79
|
+
@totals.each { |count| count_mismatch(count, @rows.size) }
|
|
80
|
+
exclude_ambiguous_records!(@rows, %i[topology_id mac host_ip producer])
|
|
78
81
|
@issues << { code: :contradictory_empty } if @empty && @rows.any?
|
|
79
82
|
@issues << { code: :missing_completeness_evidence } if @rows.empty? && !@empty && @totals != [ 0 ]
|
|
80
83
|
@issues << { code: :output_incomplete } unless complete
|
|
@@ -82,47 +85,53 @@ module Vxlan
|
|
|
82
85
|
end
|
|
83
86
|
|
|
84
87
|
# 归一化单条 EVPN 路由及回显的身份字段。
|
|
85
|
-
def
|
|
86
|
-
values[:topology_id] =
|
|
88
|
+
def parse_route_fields(values, type)
|
|
89
|
+
values[:topology_id] = parse_uint32(values[:topology_id])
|
|
87
90
|
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)
|
|
88
91
|
values[:mac] = CtxParser::Values.mac_address(values[:mac])
|
|
89
|
-
values[:sequence] =
|
|
90
|
-
values[:host_ip] =
|
|
91
|
-
values[:next_hops] =
|
|
92
|
+
values[:sequence] = parse_uint32(values[:sequence]) if values[:sequence]
|
|
93
|
+
values[:host_ip] = parse_ip_address(values.fetch(:host_ip)) if type == :mac_ip
|
|
94
|
+
values[:next_hops] = parse_next_hops(values.fetch(:next_hops))
|
|
92
95
|
values
|
|
93
96
|
end
|
|
94
97
|
|
|
95
98
|
# 归一化 EVPN 路由关联的全部下一跳。
|
|
96
|
-
def
|
|
99
|
+
def parse_next_hops(text)
|
|
97
100
|
result = []
|
|
98
|
-
|
|
99
|
-
|
|
101
|
+
# 字节视图让偏移定位保持线性;逐项扫描,不重复复制剩余长行。
|
|
102
|
+
encoding = text.encoding
|
|
103
|
+
text = text.b
|
|
104
|
+
offset = 0
|
|
105
|
+
while offset < text.bytesize
|
|
106
|
+
match = NEXT_HOP.match(text, offset)
|
|
100
107
|
raise ArgumentError unless match
|
|
101
|
-
token = match[:hop]
|
|
108
|
+
token = match[:hop].force_encoding(encoding)
|
|
102
109
|
value = case token
|
|
103
110
|
when /\ALocal\z/i then { local: true }
|
|
104
|
-
when /\AIfindex\s+(\d+)\z/i then { ifindex:
|
|
111
|
+
when /\AIfindex\s+(\d+)\z/i then { ifindex: parse_uint32($1) }
|
|
105
112
|
when /\A(?:Eth|Po|nve|Lo|Vlan|vPC)/i then { interface: token }
|
|
106
|
-
else { address:
|
|
113
|
+
else { address: parse_ip_address(token) }
|
|
107
114
|
end
|
|
108
|
-
value[:label] =
|
|
115
|
+
value[:label] = parse_uint32(match[:label]) if match[:label]
|
|
109
116
|
result << value
|
|
110
|
-
|
|
111
|
-
break if
|
|
112
|
-
|
|
113
|
-
|
|
117
|
+
offset = match.end(0)
|
|
118
|
+
break if offset == text.bytesize
|
|
119
|
+
|
|
120
|
+
separator = NEXT_HOP_SEPARATOR.match(text, offset)
|
|
121
|
+
raise ArgumentError unless separator && separator.end(0) < text.bytesize
|
|
122
|
+
offset = separator.end(0)
|
|
114
123
|
end
|
|
115
124
|
raise ArgumentError if result.empty?
|
|
116
125
|
result
|
|
117
126
|
end
|
|
118
127
|
|
|
119
128
|
# 解析数值测量,不将未知文本转换为零。
|
|
120
|
-
def
|
|
129
|
+
def parse_uint32(value)
|
|
121
130
|
CtxParser::Values.integer(value, max: UINT32.end)
|
|
122
131
|
end
|
|
123
132
|
|
|
124
133
|
# 归一化主机地址,保留 IPv4 与 IPv6 语义。
|
|
125
|
-
def
|
|
134
|
+
def parse_ip_address(value)
|
|
126
135
|
CtxParser::Values.ip_address(value, allow_zone: true)
|
|
127
136
|
end
|
|
128
137
|
end
|
|
@@ -25,7 +25,7 @@ module Vxlan
|
|
|
25
25
|
return CtxParser::Result.new(status: :partial, issues: [ { code: :invalid_encoding } ])
|
|
26
26
|
end
|
|
27
27
|
if Terminal.invalid_output?(text)
|
|
28
|
-
line = text.each_line.with_index(1).find { |source, _number| Terminal.invalid_output?(source) }.last
|
|
28
|
+
line = text.each_line("\n").with_index(1).find { |source, _number| Terminal.invalid_output?(source) }.last
|
|
29
29
|
return CtxParser::Result.new(status: :partial, issues: [ { code: :invalid_terminal_output, line: line } ])
|
|
30
30
|
end
|
|
31
31
|
if self.class::UNSUPPORTED&.match?(text)
|
|
@@ -53,7 +53,7 @@ module Vxlan
|
|
|
53
53
|
@issues << { code: :format_unrecognized } unless @header || @empty
|
|
54
54
|
@issues << { code: :contradictory_empty } if @empty && @rows.any?
|
|
55
55
|
@issues << { code: :output_incomplete } unless complete
|
|
56
|
-
|
|
56
|
+
exclude_ambiguous_records!(@rows, self.class::IDENTITY) if self.class::IDENTITY
|
|
57
57
|
CtxParser::Result.new(status: @issues.empty? ? :complete : :partial, records: @rows, issues: @issues)
|
|
58
58
|
end
|
|
59
59
|
|
|
@@ -71,8 +71,8 @@ module Vxlan
|
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
# 逐行提供有效表格内容,并记录明确空表证据。
|
|
74
|
-
def
|
|
75
|
-
text.each_line.with_index(1) do |source, number|
|
|
74
|
+
def each_content_line(text, separators: false)
|
|
75
|
+
text.each_line("\n").with_index(1) do |source, number|
|
|
76
76
|
line = source.strip
|
|
77
77
|
next if line.empty? || (!separators && line.match?(/\A[-=+\s]+\z/))
|
|
78
78
|
if self.class::EMPTY && line.match?(/\A(?:No #{self.class::EMPTY}(?: found)?|0 #{self.class::EMPTY})\.?\z/i)
|
|
@@ -84,12 +84,12 @@ module Vxlan
|
|
|
84
84
|
end
|
|
85
85
|
|
|
86
86
|
# 记录声明总数与实际解析数量不一致的情况。
|
|
87
|
-
def
|
|
87
|
+
def count_mismatch(expected, observed)
|
|
88
88
|
@issues << { code: :count_mismatch, expected: expected, observed: observed } unless expected == observed
|
|
89
89
|
end
|
|
90
90
|
|
|
91
|
-
#
|
|
92
|
-
def
|
|
91
|
+
# 追加行诊断;调用方负责决定是否继续处理当前记录。
|
|
92
|
+
def unparsed_records(number)
|
|
93
93
|
@issues << { code: :unparsed_row, line: number }
|
|
94
94
|
end
|
|
95
95
|
|
|
@@ -98,8 +98,8 @@ module Vxlan
|
|
|
98
98
|
CtxParser::Values.ip_address?(value, allow_zone: true)
|
|
99
99
|
end
|
|
100
100
|
|
|
101
|
-
#
|
|
102
|
-
def
|
|
101
|
+
# 原地排除重复身份的全部记录,不选取其中任意一条。
|
|
102
|
+
def exclude_ambiguous_records!(rows, fields, &identity)
|
|
103
103
|
identities = CtxParser::Records.new(rows, identity: fields, &identity)
|
|
104
104
|
return unless identities.duplicates?
|
|
105
105
|
rows.replace(identities.unambiguous)
|
|
@@ -20,7 +20,7 @@ module Vxlan
|
|
|
20
20
|
# 解析命令表格行,保留数量及格式错误行诊断。
|
|
21
21
|
def parse_rows(text)
|
|
22
22
|
scope, prefix_columns, prefix_seen, peers = {}, nil, {}, {}
|
|
23
|
-
|
|
23
|
+
each_content_line(text) do |line, number|
|
|
24
24
|
if HEADER.match?(line)
|
|
25
25
|
@header = true
|
|
26
26
|
elsif (match = /\ABGP summary information for VRF (\S+), address family (.+)/.match(line))
|
|
@@ -28,7 +28,7 @@ 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
|
-
|
|
31
|
+
unparsed_records(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
|
|
@@ -39,14 +39,14 @@ module Vxlan
|
|
|
39
39
|
peer[:prefixes_by_type] = prefix_columns.zip(words.drop(4).map(&:to_i)).to_h
|
|
40
40
|
prefix_seen[peer.object_id] = true
|
|
41
41
|
else
|
|
42
|
-
|
|
42
|
+
unparsed_records(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
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
|
+
unparsed_records(number)
|
|
50
50
|
next
|
|
51
51
|
end
|
|
52
52
|
row[:state] = row[:state].downcase.tr(" ", "_")
|
|
@@ -56,7 +56,7 @@ module Vxlan
|
|
|
56
56
|
# 统计表按完整作用域查找;重复邻居仍由最终身份检查整体隔离。
|
|
57
57
|
peers[row.values_at(:vrf, :address_family, :peer_ip)] ||= row
|
|
58
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)
|
|
59
|
-
|
|
59
|
+
unparsed_records(number)
|
|
60
60
|
end
|
|
61
61
|
end
|
|
62
62
|
if prefix_columns && prefix_seen.size != @rows.size
|
|
@@ -67,14 +67,14 @@ module Vxlan
|
|
|
67
67
|
# 归一化平台值,同时保留未知或不支持的状态。
|
|
68
68
|
def normalize(text)
|
|
69
69
|
output = []
|
|
70
|
-
super.each_line do |line|
|
|
70
|
+
super.each_line("\n") do |line|
|
|
71
71
|
if output.last&.strip&.end_with?("State/") && line.strip == "PfxRcd"
|
|
72
72
|
output[-1] = output.last.rstrip + line.strip + "\n"
|
|
73
73
|
else
|
|
74
74
|
output << line
|
|
75
75
|
end
|
|
76
76
|
end
|
|
77
|
-
output.join
|
|
77
|
+
output.join("")
|
|
78
78
|
end
|
|
79
79
|
end
|
|
80
80
|
end
|
|
@@ -15,7 +15,7 @@ module Vxlan
|
|
|
15
15
|
# 解析当前命令的记录,保留无法识别行的诊断。
|
|
16
16
|
def parse_rows(text)
|
|
17
17
|
row, scope, totals = nil, nil, []
|
|
18
|
-
|
|
18
|
+
each_content_line(text, separators: true) do |line, number|
|
|
19
19
|
if line.match?(/\A[-=+\s]+\z/)
|
|
20
20
|
scope = nil
|
|
21
21
|
next
|
|
@@ -33,11 +33,11 @@ module Vxlan
|
|
|
33
33
|
next
|
|
34
34
|
end
|
|
35
35
|
unless row
|
|
36
|
-
|
|
36
|
+
unparsed_records(number)
|
|
37
37
|
next
|
|
38
38
|
end
|
|
39
39
|
case line
|
|
40
|
-
when /\A--More--/i then
|
|
40
|
+
when /\A--More--/i then unparsed_records(number)
|
|
41
41
|
when /\ASystem Name:\s*(\S.*)\z/i then row[:neighbor_name] = $1
|
|
42
42
|
when /\AInterface address\(es\):(?:\s+\d+)?\z/i then scope = :interface_addresses
|
|
43
43
|
when /\AMgmt address\(es\):(?:\s+\d+)?\z/i then scope = :management_addresses
|
|
@@ -46,13 +46,13 @@ module Vxlan
|
|
|
46
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
|
+
unparsed_records(number)
|
|
50
50
|
end
|
|
51
51
|
when /\APlatform:\s*(.+?),\s*Capabilities:\s*(.+)\z/i
|
|
52
52
|
row.merge!(platform: $1, capabilities: $2)
|
|
53
53
|
scope = nil
|
|
54
54
|
when /\AInterface:\s*(\S+),\s*Port ID \(outgoing port\):\s*(\S.*)\z/i
|
|
55
|
-
|
|
55
|
+
unparsed_records(number) if row.key?(:interface)
|
|
56
56
|
row.merge!(interface: $1, neighbor_interface: $2)
|
|
57
57
|
scope = nil
|
|
58
58
|
when /\AHoldtime:\s*(\d+) sec\z/i then row[:holdtime] = $1.to_i
|
|
@@ -66,11 +66,11 @@ module Vxlan
|
|
|
66
66
|
row[:attributes][$1.downcase.tr(" ", "_").to_sym] = $2
|
|
67
67
|
scope = nil
|
|
68
68
|
else
|
|
69
|
-
scope == :version ? row[:version] = [ row[:version], line ].reject(&:empty?).join("\n") :
|
|
69
|
+
scope == :version ? row[:version] = [ row[:version], line ].reject(&:empty?).join("\n") : unparsed_records(number)
|
|
70
70
|
end
|
|
71
71
|
end
|
|
72
72
|
@rows << row if row
|
|
73
|
-
totals.each { |total|
|
|
73
|
+
totals.each { |total| count_mismatch(total, @rows.size) }
|
|
74
74
|
@empty = true if totals == [ 0 ] && @rows.empty?
|
|
75
75
|
@rows.reject! do |entry|
|
|
76
76
|
entry[:neighbor_name] ||= entry[:device_id].sub(/\([^)]*\)\z/, "").split(".").first
|