vxlan 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +74 -0
- data/README-zh.md +32 -4
- data/README.md +32 -4
- data/bin/README.md +203 -0
- data/bin/vxlan +8 -0
- data/docs/architecture-zh.md +62 -60
- data/docs/evidence-contracts.md +54 -0
- data/docs/remediation.md +84 -0
- data/docs/review-2026-10-19.md +31 -0
- data/docs/superpowers/plans/2026-10-19-command-line.md +69 -0
- data/docs/superpowers/plans/2026-10-19-connector-integration.md +83 -0
- data/docs/superpowers/plans/2026-10-19-evidence-contracts.md +64 -0
- data/docs/superpowers/plans/2026-10-19-vxlan-examples.md +47 -0
- data/docs/superpowers/plans/2026-10-20-fabric-consistency.md +28 -0
- data/docs/superpowers/specs/2026-10-19-command-line-design.md +78 -0
- data/docs/superpowers/specs/2026-10-19-connector-integration-design.md +100 -0
- data/docs/superpowers/specs/2026-10-19-evidence-contracts.md +11 -0
- data/examples/README.md +20 -0
- data/examples/collect.rb +8 -0
- data/examples/parse.rb +5 -0
- data/lib/vxlan/acl_policy.rb +50 -0
- data/lib/vxlan/change_plan/svi_acl.rb +76 -0
- data/lib/vxlan/change_plan/svi_gateway.rb +36 -0
- data/lib/vxlan/change_plan/vlan_vni.rb +40 -0
- data/lib/vxlan/change_plan.rb +131 -0
- data/lib/vxlan/cisco_nxos/configuration_state.rb +1 -4
- data/lib/vxlan/cisco_nxos/evpn_l2route.rb +2 -2
- data/lib/vxlan/cisco_nxos/parser.rb +2 -2
- data/lib/vxlan/cisco_nxos/parsers/show_bgp_l2vpn_evpn_summary.rb +67 -13
- data/lib/vxlan/cisco_nxos/parsers/show_cdp_neighbors_detail.rb +32 -8
- data/lib/vxlan/cisco_nxos/parsers/show_mac_address_table.rb +1 -1
- data/lib/vxlan/cisco_nxos/parsers/show_nve_peers.rb +5 -4
- data/lib/vxlan/cisco_nxos/parsers/show_nve_vni.rb +9 -1
- data/lib/vxlan/cisco_nxos/parsers/show_running_config.rb +40 -8
- data/lib/vxlan/cisco_nxos/parsers/show_vpc_brief.rb +11 -5
- data/lib/vxlan/cisco_nxos/syntax/policies.rb +16 -0
- data/lib/vxlan/cisco_nxos/syntax/routing.rb +16 -6
- data/lib/vxlan/cli/apply.rb +172 -0
- data/lib/vxlan/cli/collect.rb +210 -0
- data/lib/vxlan/cli/connector.rb +23 -0
- data/lib/vxlan/cli/options.rb +311 -0
- data/lib/vxlan/cli/parse.rb +58 -0
- data/lib/vxlan/cli/report.rb +223 -0
- data/lib/vxlan/cli/review/fabric.rb +102 -0
- data/lib/vxlan/cli/review/report.rb +165 -0
- data/lib/vxlan/cli/review.rb +125 -0
- data/lib/vxlan/cli/templates/env.example +12 -0
- data/lib/vxlan/cli/templates/vxlan.yml +32 -0
- data/lib/vxlan/cli.rb +41 -0
- data/lib/vxlan/collector.rb +59 -28
- data/lib/vxlan/config/evidence.rb +18 -0
- data/lib/vxlan/config/review.rb +56 -0
- data/lib/vxlan/{configuration.rb → config.rb} +1 -1
- data/lib/vxlan/fabric/review.rb +324 -0
- data/lib/vxlan/fabric/scope.rb +43 -0
- data/lib/vxlan/observation.rb +3 -0
- data/lib/vxlan/recommendations.rb +193 -0
- data/lib/vxlan/service/catalog.rb +77 -0
- data/lib/vxlan/service/mapping.rb +53 -0
- data/lib/vxlan/snapshot.rb +3 -0
- data/lib/vxlan/subnet_index.rb +88 -0
- data/lib/vxlan/version.rb +1 -1
- data/lib/vxlan.rb +4 -2
- metadata +137 -7
- data/lib/vxlan/cisco_nxos/configuration.rb +0 -10
- data/lib/vxlan/configuration_review.rb +0 -54
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "json"
|
|
5
|
+
require "ipaddr"
|
|
6
|
+
require "ctx_parser/immutable"
|
|
7
|
+
require_relative "acl_policy"
|
|
8
|
+
|
|
9
|
+
module Vxlan
|
|
10
|
+
# 一个已校验、不可变的业务增量;命令、适用前提和回读由同一对象负责。
|
|
11
|
+
class ChangePlan
|
|
12
|
+
FIELDS = %w[kind vlan vni].freeze
|
|
13
|
+
EMPTY_ADDRESSES = [].freeze
|
|
14
|
+
private_constant :EMPTY_ADDRESSES
|
|
15
|
+
attr_reader :commands, :rollback
|
|
16
|
+
|
|
17
|
+
# 类型判别只发生在输入边界,不接受任意脚本或未声明字段。
|
|
18
|
+
def self.build(change)
|
|
19
|
+
raise ArgumentError, "增量须为映射" unless change.is_a?(Hash)
|
|
20
|
+
case change["kind"]
|
|
21
|
+
when "vlan_vni_add" then VlanVni.new(change)
|
|
22
|
+
when "svi_gateway_add" then SviGateway.new(change)
|
|
23
|
+
when "svi_acl" then SviAcl.new(change)
|
|
24
|
+
else raise ArgumentError, "不支持的增量类型"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# 审查中的未知或冲突仍为待核实建议,不能构造可执行计划。
|
|
29
|
+
def self.candidate(change, config:)
|
|
30
|
+
plan = build(change)
|
|
31
|
+
plan if plan.eligible?(config)
|
|
32
|
+
rescue ArgumentError
|
|
33
|
+
nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# 字符串键规范化后计算语义指纹;Hash 顺序不影响结果,数组保留业务顺序。
|
|
37
|
+
def self.fingerprint(value) = Digest::SHA256.hexdigest(JSON.generate(canonical(value)))
|
|
38
|
+
|
|
39
|
+
# 规范化同时提供独立容器,用于屏蔽唯一允许变化的字段。
|
|
40
|
+
def self.canonical(value)
|
|
41
|
+
case value
|
|
42
|
+
when Hash then value.transform_keys(&:to_s).sort.to_h.transform_values { |entry| canonical(entry) }
|
|
43
|
+
when Array then value.map { |entry| canonical(entry) }
|
|
44
|
+
else value
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# 构造时一次校验和渲染,随后调用者不能改变已确认的参数或命令。
|
|
49
|
+
def initialize(change)
|
|
50
|
+
raise ArgumentError, "请使用 ChangePlan.build 构造业务增量" if instance_of?(ChangePlan)
|
|
51
|
+
unless change.is_a?(Hash) && change.keys.all?(String) && (change.keys - self.class::FIELDS).empty? && change["kind"] == self.class::KIND
|
|
52
|
+
raise ArgumentError, "增量字段无效"
|
|
53
|
+
end
|
|
54
|
+
@change = CtxParser::Immutable.copy(change)
|
|
55
|
+
validate_parameters!
|
|
56
|
+
@commands, @rollback = CtxParser::Immutable.copy(render)
|
|
57
|
+
freeze
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# 报告边界保持普通数据及原有指纹,不引入对象序列化格式。
|
|
61
|
+
def to_h = @change
|
|
62
|
+
|
|
63
|
+
# 不完整事实不能满足动态前提;输入错误不能变成肯定结果。
|
|
64
|
+
def eligible?(config)
|
|
65
|
+
config.is_a?(Hash) && eligible_state?(config) == true
|
|
66
|
+
rescue ArgumentError, KeyError, TypeError
|
|
67
|
+
false
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# 只允许从合格起点到目标状态;其余已建模业务事实必须完整保留。
|
|
71
|
+
def satisfied?(after, before:)
|
|
72
|
+
return false unless eligible?(before) && after.is_a?(Hash) && mapped_svi(after)
|
|
73
|
+
preserved_context(before) == preserved_context(after) && target_state?(after) == true
|
|
74
|
+
rescue ArgumentError, KeyError, TypeError
|
|
75
|
+
false
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
# 三种动作均定位已有 L2 业务,VLAN/VNI 不从字符串或名称猜测。
|
|
81
|
+
def validate_parameters!
|
|
82
|
+
raise ArgumentError, "VLAN 无效" unless @change["vlan"].is_a?(Integer) && @change["vlan"].between?(1, 4094)
|
|
83
|
+
raise ArgumentError, "VNI 无效" unless @change["vni"].is_a?(Integer) && @change["vni"].between?(1, 16_777_215)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# 网关相关增量必须携带明确租户及单个已验证 IPv4 前缀。
|
|
87
|
+
def validate_gateway!
|
|
88
|
+
address = @change["address"]
|
|
89
|
+
unless @change["vrf"].is_a?(String) && !@change["vrf"].empty? && address.is_a?(String) &&
|
|
90
|
+
address.match?(/\A\d{1,3}(?:\.\d{1,3}){3}\/\d{1,2}\z/) && IPAddr.new(address).ipv4?
|
|
91
|
+
raise ArgumentError, "缺少已验证的业务网关"
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# 目标接口由已验证 VLAN 唯一定位,不接受自由接口表达式。
|
|
96
|
+
def interface = "Vlan#{@change['vlan']}"
|
|
97
|
+
|
|
98
|
+
# 关联存在且未指向其他业务时才有可变更的 SVI。
|
|
99
|
+
def mapped_svi(config)
|
|
100
|
+
config.dig("interfaces", interface) if config.fetch("vlan_vnis", {})[@change["vlan"].to_s] == @change["vni"]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# 网关增量不能改租户、Anycast 模式或与本机其他业务网段重叠。
|
|
104
|
+
def gateway_context?(config)
|
|
105
|
+
target_interface = interface
|
|
106
|
+
svi = config.dig("interfaces", target_interface)
|
|
107
|
+
return false unless svi && svi["anycast"] == true && svi["vrf"] == @change["vrf"]
|
|
108
|
+
network = IPAddr.new(@change.fetch("address"))
|
|
109
|
+
config.fetch("interfaces", {}).each do |name, other|
|
|
110
|
+
next if name == target_interface || other["vrf"] != @change["vrf"]
|
|
111
|
+
return false if other.fetch("addresses", EMPTY_ADDRESSES).any? do |address|
|
|
112
|
+
candidate = IPAddr.new(address)
|
|
113
|
+
network.include?(candidate) || candidate.include?(network)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
true
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# 元数据与可推导事实不参与漂移比较;每种动作只移除其允许变化的字段。
|
|
120
|
+
def preserved_context(config)
|
|
121
|
+
context = ChangePlan.canonical(config).except("config_digest", "review_status", "review_findings", "segments", "unnumbered_addresses")
|
|
122
|
+
context.fetch("acls", {}).transform_values! { |entries| AclPolicy.rules(entries) }
|
|
123
|
+
remove_changed_fields(context)
|
|
124
|
+
context
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
require_relative "change_plan/vlan_vni"
|
|
130
|
+
require_relative "change_plan/svi_gateway"
|
|
131
|
+
require_relative "change_plan/svi_acl"
|
|
@@ -11,7 +11,7 @@ module Vxlan
|
|
|
11
11
|
# 初始化单次配置解析使用的可变 NX-OS 事实容器。
|
|
12
12
|
def initialize
|
|
13
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: [] }
|
|
14
|
+
evpn: {}, bgp: {}, ospf: {}, vpc_domain: nil, unnumbered_addresses: {}, prefix_lists: {}, ipv6_prefix_lists: {}, route_maps: {}, acls: {}, address_groups: {}, static_routes: [] }
|
|
15
15
|
@issues = []
|
|
16
16
|
end
|
|
17
17
|
|
|
@@ -27,9 +27,6 @@ module Vxlan
|
|
|
27
27
|
data
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
# 兼容原有解析状态的收尾入口。
|
|
31
|
-
alias finish finalize_facts
|
|
32
|
-
|
|
33
30
|
# 展示对象身份与状态,避免泄露原始回显或凭据。
|
|
34
31
|
def inspect = "#<#{self.class} interfaces=#{data[:interfaces].size} issues=#{issues.size}>"
|
|
35
32
|
|
|
@@ -22,9 +22,9 @@ module Vxlan
|
|
|
22
22
|
type = self.class::TYPE
|
|
23
23
|
@totals = []
|
|
24
24
|
fields = previous = next_hop_index = nil
|
|
25
|
-
text.each_line.with_index(1) do |source, number|
|
|
25
|
+
text.each_line("\n").with_index(1) do |source, number|
|
|
26
26
|
line = source.strip
|
|
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/)
|
|
27
|
+
next if line.empty? || line.match?(/\A[-=\s]+\z/) || line.match?(/\A(?:(?:NH\s+)?Flags\s*-\s*)?(?:\([A-Za-z0-9]+\):[A-Za-z0-9 ,\/-]+\s*)+\z/)
|
|
28
28
|
if line.match?(/\ATopology\b/i)
|
|
29
29
|
labels = line.gsub(/\s+/, " ").scan(HEADER)
|
|
30
30
|
fields = labels.map { |label| LABELS.fetch(label.downcase) }
|
|
@@ -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)
|
|
@@ -72,7 +72,7 @@ module Vxlan
|
|
|
72
72
|
|
|
73
73
|
# 逐行提供有效表格内容,并记录明确空表证据。
|
|
74
74
|
def each_content_line(text, separators: false)
|
|
75
|
-
text.each_line.with_index(1) do |source, number|
|
|
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)
|
|
@@ -10,33 +10,86 @@ module Vxlan
|
|
|
10
10
|
EMPTY = "(?:BGP )?neighbors"
|
|
11
11
|
IDENTITY = %i[vrf address_family peer_ip].freeze
|
|
12
12
|
BOUNDARY = /^\s*BGP summary information for VRF /
|
|
13
|
+
SCOPE = /\ABGP summary information for VRF (\S+), address family (.+)\z/
|
|
14
|
+
ROUTER = /\ABGP router identifier (\S+), local AS number (\S+)\z/
|
|
13
15
|
HEADER = /\ANeighbor\s+V\s+AS\s+MsgRcvd\s+MsgSent\s+TblVer\s+InQ\s+OutQ\s+Up\/Down\s+State\/PfxRcd\z/i
|
|
14
16
|
ROW = /\A([\da-f:.]+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(.+)\z/i
|
|
15
17
|
FIELDS = %i[peer_ip version remote_as messages_received messages_sent table_version input_queue output_queue uptime state].freeze
|
|
16
18
|
INTEGERS = %i[version remote_as messages_received messages_sent table_version input_queue output_queue].freeze
|
|
19
|
+
PREFIX_TYPES = %i[type1 type2 type3 type4 type5 type6 type7 type8 type12].freeze
|
|
17
20
|
|
|
18
21
|
private
|
|
19
22
|
|
|
23
|
+
# 重复物理输出段仍属于同一逻辑 VRF/AF;空表段也必须参与身份冲突检查。
|
|
24
|
+
def parse_content(text, complete:)
|
|
25
|
+
super
|
|
26
|
+
scope, identities, invalid = nil, {}, {}
|
|
27
|
+
text.each_line("\n") do |line|
|
|
28
|
+
if (match = SCOPE.match(line.strip))
|
|
29
|
+
scope = match.captures
|
|
30
|
+
elsif (match = ROUTER.match(line.strip))
|
|
31
|
+
identity = router_identity(match)
|
|
32
|
+
if !identity
|
|
33
|
+
invalid[scope] ||= :invalid_bgp_identity
|
|
34
|
+
elsif identities.key?(scope) && identities[scope] != identity
|
|
35
|
+
invalid[scope] ||= :conflicting_bgp_identity
|
|
36
|
+
else
|
|
37
|
+
identities[scope] = identity
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
invalid.each do |key, code|
|
|
42
|
+
issue = { code: code, vrf: key&.first, address_family: key&.last }
|
|
43
|
+
@issues << issue unless @issues.include?(issue)
|
|
44
|
+
end
|
|
45
|
+
@rows.reject! { |row| invalid.key?(row.values_at(:vrf, :address_family)) }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# ASDOT 与 ASPLAIN 用同一数值比较;无效身份不能附着到完整邻居事实。
|
|
49
|
+
def router_identity(match)
|
|
50
|
+
router, asn = match.captures
|
|
51
|
+
return unless ip_address?(router) && IPAddr.new(router).ipv4? && asn.match?(/\A\d+(?:\.\d+)?\z/)
|
|
52
|
+
parts = asn.split(".").map(&:to_i)
|
|
53
|
+
return if parts.size == 2 && parts.any? { |part| !part.between?(0, 65_535) }
|
|
54
|
+
number = parts.size == 2 ? parts.first * 65_536 + parts.last : parts.first
|
|
55
|
+
[ IPAddr.new(router).to_s, number ] if number.between?(1, 4_294_967_295)
|
|
56
|
+
end
|
|
57
|
+
|
|
20
58
|
# 解析命令表格行,保留数量及格式错误行诊断。
|
|
21
59
|
def parse_rows(text)
|
|
22
60
|
scope, prefix_columns, prefix_seen, peers = {}, nil, {}, {}
|
|
61
|
+
prefix_total = false
|
|
23
62
|
each_content_line(text) do |line, number|
|
|
24
63
|
if HEADER.match?(line)
|
|
25
64
|
@header = true
|
|
26
|
-
elsif (match =
|
|
65
|
+
elsif (match = SCOPE.match(line))
|
|
27
66
|
scope = { vrf: match[1], address_family: match[2] }
|
|
67
|
+
unparsed_records(number) unless scope[:address_family] == "L2VPN EVPN"
|
|
28
68
|
prefix_columns = nil
|
|
29
|
-
elsif (match =
|
|
30
|
-
scope.merge!(router_id: match[1]
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
prefix_columns =
|
|
69
|
+
elsif (match = ROUTER.match(line))
|
|
70
|
+
scope.merge!(router_id: match[1], local_as: match[2])
|
|
71
|
+
elsif (match = /\ANeighbor\s+T\s+AS\s+(?:(PfxRcd)\s+)?(Type-[1-9]\d*(?:\s+Type-[1-9]\d*)*)\z/.match(line))
|
|
72
|
+
prefix_total = !match[1].nil?
|
|
73
|
+
prefix_columns = match[2].split.map { |column| column.downcase.delete("-").to_sym }
|
|
74
|
+
if prefix_columns.uniq != prefix_columns || (prefix_columns - PREFIX_TYPES).any?
|
|
75
|
+
unparsed_records(number)
|
|
76
|
+
prefix_columns = nil
|
|
77
|
+
end
|
|
34
78
|
elsif prefix_columns
|
|
35
79
|
words = line.split
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
80
|
+
address = CtxParser::Values.ip_address(words[0], allow_zone: true) if ip_address?(words[0])
|
|
81
|
+
peer = peers[[ scope[:vrf], scope[:address_family], address ]]
|
|
82
|
+
offset = prefix_total ? 4 : 3
|
|
83
|
+
if words.size == prefix_columns.size + offset && %w[I E].include?(words[1]) && words.drop(2).all? { |word| word.match?(/\A\d+\z/) } &&
|
|
84
|
+
peer && peer[:prefixes_received] && !prefix_seen.key?(peer.object_id) && peer[:remote_as] == words[2].to_i &&
|
|
85
|
+
(!prefix_total || peer[:prefixes_received] == words[3].to_i)
|
|
86
|
+
counts = words.drop(offset).map(&:to_i)
|
|
87
|
+
# 路由类型是总前缀的子集;未展示的类型允许存在,已展示之和不能超过总数。
|
|
88
|
+
if counts.sum > peer[:prefixes_received]
|
|
89
|
+
@issues << { code: :inconsistent_prefix_statistics, line: number }
|
|
90
|
+
else
|
|
91
|
+
peer[:prefixes_by_type] = prefix_columns.zip(counts).to_h
|
|
92
|
+
end
|
|
40
93
|
prefix_seen[peer.object_id] = true
|
|
41
94
|
else
|
|
42
95
|
unparsed_records(number)
|
|
@@ -44,11 +97,12 @@ module Vxlan
|
|
|
44
97
|
elsif (match = ROW.match(line))
|
|
45
98
|
row = FIELDS.zip(match.captures).to_h.merge(scope)
|
|
46
99
|
INTEGERS.each { |key| row[key] = row[key].to_i }
|
|
47
|
-
unless ip_address?(row[:peer_ip]) && row[:version] == 4 && (1..4_294_967_295).cover?(row[:remote_as]) &&
|
|
100
|
+
unless row[:vrf] && row[:address_family] == "L2VPN EVPN" && ip_address?(row[:peer_ip]) && row[:version] == 4 && (1..4_294_967_295).cover?(row[:remote_as]) &&
|
|
48
101
|
row[:state].match?(/\A(?:\d+|Idle(?: \(Admin\))?|Active|Connect|OpenSent|OpenConfirm|Shutdown)\z/i)
|
|
49
102
|
unparsed_records(number)
|
|
50
103
|
next
|
|
51
104
|
end
|
|
105
|
+
row[:peer_ip] = CtxParser::Values.ip_address(row[:peer_ip], allow_zone: true)
|
|
52
106
|
row[:state] = row[:state].downcase.tr(" ", "_")
|
|
53
107
|
row[:prefixes_received] = row[:state].match?(/\A\d+\z/) ? row[:state].to_i : nil
|
|
54
108
|
row[:state] = "established" if row[:prefixes_received]
|
|
@@ -67,14 +121,14 @@ module Vxlan
|
|
|
67
121
|
# 归一化平台值,同时保留未知或不支持的状态。
|
|
68
122
|
def normalize(text)
|
|
69
123
|
output = []
|
|
70
|
-
super.each_line do |line|
|
|
124
|
+
super.each_line("\n") do |line|
|
|
71
125
|
if output.last&.strip&.end_with?("State/") && line.strip == "PfxRcd"
|
|
72
126
|
output[-1] = output.last.rstrip + line.strip + "\n"
|
|
73
127
|
else
|
|
74
128
|
output << line
|
|
75
129
|
end
|
|
76
130
|
end
|
|
77
|
-
output.join
|
|
131
|
+
output.join("")
|
|
78
132
|
end
|
|
79
133
|
end
|
|
80
134
|
end
|
|
@@ -15,6 +15,7 @@ module Vxlan
|
|
|
15
15
|
# 解析当前命令的记录,保留无法识别行的诊断。
|
|
16
16
|
def parse_rows(text)
|
|
17
17
|
row, scope, totals = nil, nil, []
|
|
18
|
+
@field_conflicts = {}.compare_by_identity
|
|
18
19
|
each_content_line(text, separators: true) do |line, number|
|
|
19
20
|
if line.match?(/\A[-=+\s]+\z/)
|
|
20
21
|
scope = nil
|
|
@@ -38,7 +39,7 @@ module Vxlan
|
|
|
38
39
|
end
|
|
39
40
|
case line
|
|
40
41
|
when /\A--More--/i then unparsed_records(number)
|
|
41
|
-
when /\ASystem Name:\s*(\S.*)\z/i then row
|
|
42
|
+
when /\ASystem Name:\s*(\S.*)\z/i then assign_field(row, :neighbor_name, $1, number)
|
|
42
43
|
when /\AInterface address\(es\):(?:\s+\d+)?\z/i then scope = :interface_addresses
|
|
43
44
|
when /\AMgmt address\(es\):(?:\s+\d+)?\z/i then scope = :management_addresses
|
|
44
45
|
when /\AIP(?:v[46])? Address:\s*(\S+)\z/i
|
|
@@ -49,21 +50,28 @@ module Vxlan
|
|
|
49
50
|
unparsed_records(number)
|
|
50
51
|
end
|
|
51
52
|
when /\APlatform:\s*(.+?),\s*Capabilities:\s*(.+)\z/i
|
|
52
|
-
|
|
53
|
+
platform, capabilities = $1, $2
|
|
54
|
+
assign_field(row, :platform, platform, number)
|
|
55
|
+
assign_field(row, :capabilities, capabilities, number)
|
|
53
56
|
scope = nil
|
|
54
57
|
when /\AInterface:\s*(\S+),\s*Port ID \(outgoing port\):\s*(\S.*)\z/i
|
|
55
|
-
|
|
56
|
-
row
|
|
58
|
+
interface, neighbor_interface = $1, $2
|
|
59
|
+
assign_field(row, :interface, interface, number)
|
|
60
|
+
assign_field(row, :neighbor_interface, neighbor_interface, number)
|
|
57
61
|
scope = nil
|
|
58
|
-
when /\AHoldtime:\s*(\d+) sec\z/i then row
|
|
62
|
+
when /\AHoldtime:\s*(\d+) sec\z/i then assign_field(row, :holdtime, $1.to_i, number)
|
|
59
63
|
when /\AVersion:\z/i
|
|
60
64
|
scope = :version
|
|
61
65
|
row[:version] = ""
|
|
62
66
|
when /\A((?:Local|Remote) Interface MAC):\s*([\da-f]{2}(?::[\da-f]{2}){5})\z/i
|
|
63
|
-
row[:attributes]
|
|
67
|
+
assign_field(row[:attributes], $1.downcase.tr(" ", "_").to_sym, $2.downcase, number)
|
|
64
68
|
scope = nil
|
|
65
|
-
when /\
|
|
66
|
-
|
|
69
|
+
when /\ADC_UUID:\s*(.*?)\z/i
|
|
70
|
+
value = $1.empty? ? nil : $1
|
|
71
|
+
assign_field(row[:attributes], :dc_uuid, value, number)
|
|
72
|
+
scope = nil
|
|
73
|
+
when /\A(Advertisement Version|Duplex|MTU|Native VLAN|VTP Management Domain|Physical Location):\s*(.+)\z/i
|
|
74
|
+
assign_field(row[:attributes], $1.downcase.tr(" ", "_").to_sym, $2, number)
|
|
67
75
|
scope = nil
|
|
68
76
|
else
|
|
69
77
|
scope == :version ? row[:version] = [ row[:version], line ].reject(&:empty?).join("\n") : unparsed_records(number)
|
|
@@ -73,6 +81,8 @@ module Vxlan
|
|
|
73
81
|
totals.each { |total| count_mismatch(total, @rows.size) }
|
|
74
82
|
@empty = true if totals == [ 0 ] && @rows.empty?
|
|
75
83
|
@rows.reject! do |entry|
|
|
84
|
+
next true if @field_conflicts.fetch(entry, {}).keys.intersect?(%i[neighbor_name interface neighbor_interface])
|
|
85
|
+
|
|
76
86
|
entry[:neighbor_name] ||= entry[:device_id].sub(/\([^)]*\)\z/, "").split(".").first
|
|
77
87
|
if entry.values_at(:neighbor_name, :interface, :neighbor_interface).any? { |value| value.nil? || value.empty? }
|
|
78
88
|
@issues << { code: :incomplete_cdp_neighbor }
|
|
@@ -80,6 +90,20 @@ module Vxlan
|
|
|
80
90
|
end
|
|
81
91
|
end
|
|
82
92
|
end
|
|
93
|
+
|
|
94
|
+
# 单值字段冲突后永久排除,不能被第三次声明或名称回退恢复。
|
|
95
|
+
def assign_field(values, key, value, number)
|
|
96
|
+
conflicts = (@field_conflicts[values] ||= {})
|
|
97
|
+
return if conflicts.key?(key)
|
|
98
|
+
|
|
99
|
+
if values.key?(key) && values[key] != value
|
|
100
|
+
values.delete(key)
|
|
101
|
+
conflicts[key] = true
|
|
102
|
+
@issues << { code: :conflicting_cdp_value, line: number, field: key }
|
|
103
|
+
else
|
|
104
|
+
values[key] = value
|
|
105
|
+
end
|
|
106
|
+
end
|
|
83
107
|
end
|
|
84
108
|
end
|
|
85
109
|
end
|
|
@@ -24,7 +24,7 @@ module Vxlan
|
|
|
24
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
|
-
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)
|
|
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)?|TL - True Learned, PS - Peer Sync, RO - Re-originate)\z/i)
|
|
28
28
|
unparsed_records(number)
|
|
29
29
|
end
|
|
30
30
|
end
|
|
@@ -10,7 +10,7 @@ module Vxlan
|
|
|
10
10
|
EMPTY = "peers"
|
|
11
11
|
IDENTITY = %i[interface peer_ip].freeze
|
|
12
12
|
HEADER = /\AInterface\s+Peer-IP\s+State\s+LearnType\s+Uptime\s+Router-Mac\z/i
|
|
13
|
-
ROW = /\A(nve\d+)\s+(\d+(?:\.\d+){3})\s+(Up|Down)\s+(\S+)\s+(\S+)\s+([\da-f]{4}(?:\.[\da-f]{4}){2})\z/i
|
|
13
|
+
ROW = /\A(nve\d+)\s+(\d+(?:\.\d+){3})\s+(Up|Down)\s+(\S+)\s+(\S+)\s+([\da-f]{4}(?:\.[\da-f]{4}){2}|n\/a)\z/i
|
|
14
14
|
FIELDS = %i[interface peer_ip state learn_type uptime router_mac].freeze
|
|
15
15
|
|
|
16
16
|
private
|
|
@@ -21,7 +21,8 @@ module Vxlan
|
|
|
21
21
|
if HEADER.match?(line)
|
|
22
22
|
@header = true
|
|
23
23
|
elsif (match = ROW.match(line)) && ip_address?(match[2])
|
|
24
|
-
@rows << FIELDS.zip(match.captures).to_h.merge(state: match[3].downcase
|
|
24
|
+
@rows << FIELDS.zip(match.captures).to_h.merge(state: match[3].downcase,
|
|
25
|
+
router_mac: match[6].casecmp?("n/a") ? nil : match[6])
|
|
25
26
|
else
|
|
26
27
|
unparsed_records(number)
|
|
27
28
|
end
|
|
@@ -31,7 +32,7 @@ module Vxlan
|
|
|
31
32
|
# 归一化平台值,同时保留未知或不支持的状态。
|
|
32
33
|
def normalize(text)
|
|
33
34
|
output = []
|
|
34
|
-
super.each_line do |line|
|
|
35
|
+
super.each_line("\n") do |line|
|
|
35
36
|
tail, previous = line.strip, output.last&.strip
|
|
36
37
|
header = previous && previous.end_with?("Route") && tail == "r-Mac"
|
|
37
38
|
mac = previous && tail.match?(/\A[\da-f]{4}\.[\da-f]{4}\z/i) && previous.match?(/\Anve\d+\s+\d+(?:\.\d+){3}\s+(?:Up|Down)\s+\S+\s+\S+\s+[\da-f]{4}\.\z/i)
|
|
@@ -41,7 +42,7 @@ module Vxlan
|
|
|
41
42
|
output << line
|
|
42
43
|
end
|
|
43
44
|
end
|
|
44
|
-
output.join
|
|
45
|
+
output.join("")
|
|
45
46
|
end
|
|
46
47
|
end
|
|
47
48
|
end
|
|
@@ -23,7 +23,8 @@ module Vxlan
|
|
|
23
23
|
elsif (match = ROW.match(line))
|
|
24
24
|
row = FIELDS.zip(match.captures).to_h.compact
|
|
25
25
|
row[:vni] = row[:vni].to_i
|
|
26
|
-
unless (1..16_777_215).cover?(row[:vni]) &&
|
|
26
|
+
unless (1..16_777_215).cover?(row[:vni]) && multicast_group?(row[:multicast_group]) &&
|
|
27
|
+
(!row[:flags] || %w[SA S-ND SU Xconn MS-IR HYB].include?(row[:flags]))
|
|
27
28
|
unparsed_records(number)
|
|
28
29
|
next
|
|
29
30
|
end
|
|
@@ -33,6 +34,13 @@ module Vxlan
|
|
|
33
34
|
end
|
|
34
35
|
end
|
|
35
36
|
end
|
|
37
|
+
|
|
38
|
+
# 此列也承载单播复制标记;其余值必须是 IPv4 组播主机地址。
|
|
39
|
+
def multicast_group?(value)
|
|
40
|
+
return true if %w[UnicastBGP UnicastStatic n/a].include?(value)
|
|
41
|
+
|
|
42
|
+
ip_address?(value) && IPAddr.new("224.0.0.0/4").include?(value)
|
|
43
|
+
end
|
|
36
44
|
end
|
|
37
45
|
end
|
|
38
46
|
end
|
|
@@ -6,8 +6,8 @@ require_relative "../syntax/interfaces"
|
|
|
6
6
|
require_relative "../syntax/routing"
|
|
7
7
|
require_relative "../syntax/policies"
|
|
8
8
|
require_relative "../document"
|
|
9
|
-
require_relative "../
|
|
10
|
-
require_relative "../../
|
|
9
|
+
require_relative "../configuration_state"
|
|
10
|
+
require_relative "../../config/review"
|
|
11
11
|
require "ctx_parser/configuration"
|
|
12
12
|
require "ctx_parser/values"
|
|
13
13
|
|
|
@@ -47,7 +47,7 @@ module Vxlan
|
|
|
47
47
|
@issues << { code: :output_incomplete } unless complete
|
|
48
48
|
status = @issues.empty? ? :complete : :partial
|
|
49
49
|
data[:review_status] = status
|
|
50
|
-
data[:review_findings] = Vxlan::
|
|
50
|
+
data[:review_findings] = Vxlan::Config::Review.new(data).call(complete: status == :complete)
|
|
51
51
|
CtxParser::Result.new(status: status, records: [ data ], issues: @issues)
|
|
52
52
|
end
|
|
53
53
|
|
|
@@ -111,7 +111,7 @@ module Vxlan
|
|
|
111
111
|
analyze_children(block) do |child|
|
|
112
112
|
if (match = /\Avni (\d+) (l[23])\z/i.match(child.text))
|
|
113
113
|
id = integer(match[1], 1..16_777_215)
|
|
114
|
-
store_unique_fact(data[:evpn], id, child, path: [ :evpn, id ]) { vrf(child).merge(type: match[2].downcase) }
|
|
114
|
+
store_unique_fact(data[:evpn], id, child, path: [ :evpn, id ]) { vrf(child, address_families: false).merge(type: match[2].downcase) }
|
|
115
115
|
else
|
|
116
116
|
issue(child)
|
|
117
117
|
end
|
|
@@ -135,6 +135,9 @@ module Vxlan
|
|
|
135
135
|
name, action, sequence = $1, $2, integer($3, 1..65_535)
|
|
136
136
|
entries = (data[:route_maps][name] ||= [])
|
|
137
137
|
policy_entry(:route_maps, name, sequence, entries, block) { route_map(block).merge(action: action, sequence: sequence) }
|
|
138
|
+
when /\Aobject-group ip address (\S+)\z/
|
|
139
|
+
name = $1
|
|
140
|
+
store_unique_fact(data[:address_groups], name, block, path: [ :address_groups, name ]) { address_group(block) }
|
|
138
141
|
when /\Aip access-list (\S+)\z/
|
|
139
142
|
name = $1
|
|
140
143
|
entries = block.children.filter_map do |child|
|
|
@@ -149,14 +152,19 @@ module Vxlan
|
|
|
149
152
|
end
|
|
150
153
|
end
|
|
151
154
|
end
|
|
152
|
-
store_unique_fact(data[:acls], name, block, path: [ :acls, name ])
|
|
155
|
+
store_unique_fact(data[:acls], name, block, path: [ :acls, name ]) do
|
|
156
|
+
raise ArgumentError unless entries.map { |entry| entry[:sequence] }.uniq.size == entries.size
|
|
157
|
+
entries
|
|
158
|
+
end
|
|
153
159
|
when /\Aip route /
|
|
154
160
|
data[:static_routes] << scope(block, [ :static_routes, data[:static_routes].size ]) { static_route(block) }
|
|
155
161
|
when "nv overlay evpn", "no nv overlay evpn"
|
|
156
162
|
assign(data, :nv_overlay_evpn, block.text == "nv overlay evpn", block)
|
|
157
163
|
capture(block, data[:nv_overlay_evpn], [ :nv_overlay_evpn ]) if data.key?(:nv_overlay_evpn)
|
|
158
164
|
when /\Afabric forwarding anycast-gateway-mac (\S+)\z/
|
|
159
|
-
|
|
165
|
+
mac = $1
|
|
166
|
+
CtxParser::Values.mac_address(mac)
|
|
167
|
+
assign(data, :anycast_mac, mac, block)
|
|
160
168
|
capture(block, data[:anycast_mac], [ :anycast_mac ]) if data.key?(:anycast_mac)
|
|
161
169
|
when /\A(?:interface|vlan|vrf|router|vpc|evpn|feature|ip prefix-list|ipv6 prefix-list|route-map)\b/ then issue(block)
|
|
162
170
|
when /\A(?:%|ERROR:|Invalid command|--More--)/i then issue(block, :invalid_command_output)
|
|
@@ -166,7 +174,7 @@ module Vxlan
|
|
|
166
174
|
end
|
|
167
175
|
|
|
168
176
|
# 解析 VRF 定义及其 VNI 或 route-target 关联。
|
|
169
|
-
def vrf(block)
|
|
177
|
+
def vrf(block, address_families: true)
|
|
170
178
|
values = { route_targets: [], routes: [] }
|
|
171
179
|
analyze_children(block) do |child|
|
|
172
180
|
case child.text
|
|
@@ -175,6 +183,8 @@ module Vxlan
|
|
|
175
183
|
when /\Aroute-target / then values[:route_targets] << route_target(child)
|
|
176
184
|
when /\Aaddress-family (.+)\z/
|
|
177
185
|
family = $1
|
|
186
|
+
next issue(child) unless address_families && %w[ipv4\ unicast ipv6\ unicast].include?(family)
|
|
187
|
+
|
|
178
188
|
targets_start = values[:route_targets].size
|
|
179
189
|
exports_start = values.fetch(:exports, []).size
|
|
180
190
|
analyze_children(child) do |setting|
|
|
@@ -242,9 +252,31 @@ module Vxlan
|
|
|
242
252
|
def static_route(block)
|
|
243
253
|
words = block.text.split
|
|
244
254
|
raise ArgumentError if words.size < 4
|
|
255
|
+
validate_static_next_hop(words.drop(3))
|
|
256
|
+
|
|
245
257
|
{ prefix: ipv4_prefix(words[2]), next_hop: words[3], options: words.drop(4).join(" ") }
|
|
246
258
|
end
|
|
247
259
|
|
|
260
|
+
# IP route 的下一跳是 IPv4 或出接口;接口分词形式不能误当作地址拒绝。
|
|
261
|
+
def validate_static_next_hop(words)
|
|
262
|
+
hop = words.first
|
|
263
|
+
interface = /\A(?:ethernet\d+(?:\/\d+){1,2}(?:\.\d+)?|port-channel\d+(?:\.\d+)?|vlan\d+|mgmt\d+|null0)\z/i
|
|
264
|
+
offset = if interface.match?(hop)
|
|
265
|
+
1
|
|
266
|
+
elsif /\A(?:ethernet|port-channel|vlan|mgmt|null)\z/i.match?(hop)
|
|
267
|
+
raise ArgumentError unless words[1] && interface.match?(hop + words[1])
|
|
268
|
+
|
|
269
|
+
2
|
|
270
|
+
else
|
|
271
|
+
0
|
|
272
|
+
end
|
|
273
|
+
hop = words[offset]
|
|
274
|
+
# 出接口之后可直接跟已有选项;其余词须先作为 IPv4 下一跳校验。
|
|
275
|
+
return if offset.positive? && (hop.nil? || /\A(?:name|tag|track|vrf|\d+)\z/.match?(hop))
|
|
276
|
+
|
|
277
|
+
raise ArgumentError unless IPAddr.new(ip_address(hop)).ipv4?
|
|
278
|
+
end
|
|
279
|
+
|
|
248
280
|
# 解析十进制整数,并校验声明的边界。
|
|
249
281
|
def integer(value, range)
|
|
250
282
|
raise ArgumentError unless value.is_a?(String)
|
|
@@ -311,7 +343,7 @@ module Vxlan
|
|
|
311
343
|
pending = [ block ]
|
|
312
344
|
until pending.empty?
|
|
313
345
|
entry = pending.pop
|
|
314
|
-
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)
|
|
346
|
+
if /\A(?:no )?(?:hostname|interface|vlan|vrf|router|vpc|evpn|feature|object-group|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)
|
|
315
347
|
issue(entry)
|
|
316
348
|
else
|
|
317
349
|
@cursor.mark(entry.index, state: :ignored)
|
|
@@ -19,24 +19,29 @@ module Vxlan
|
|
|
19
19
|
section = :peer_links
|
|
20
20
|
elsif line.match?(/\Aid\s+Port\s+Status\s+Consistency\s+Reason\s+Active vlans\z/i)
|
|
21
21
|
section = :members
|
|
22
|
-
elsif (match = /\A(\d+)\s+(Po\d+)\s+(up|down)\s+(.+)\z/i.match(line)) && section
|
|
23
|
-
row = { id: match[1].to_i, interface: match[2], state: match[3].downcase }
|
|
22
|
+
elsif (match = /\A(\d+)\s+(Po\d+)\s+(up|down\*?)\s+(.+)\z/i.match(line)) && section
|
|
23
|
+
row = { id: match[1].to_i, interface: match[2], state: match[3].downcase.delete_suffix("*") }
|
|
24
|
+
row[:forwarding_via_peer_link] = true if match[3].end_with?("*")
|
|
24
25
|
rest = match[4].split(/\s{2,}/)
|
|
25
26
|
if section == :members
|
|
26
|
-
member = /\A(\S+)\s+(.+?)\s
|
|
27
|
+
member = /\A(\S+)\s+(.+?)\s+([\d, .-]+)\z/.match(match[4])
|
|
27
28
|
unless member
|
|
28
29
|
unparsed_records(number)
|
|
29
30
|
next
|
|
30
31
|
end
|
|
31
32
|
row.merge!(consistency: member[1], reason: member[2], active_vlans_summary: member[3])
|
|
32
33
|
elsif match[4].match?(/\A[\d, .-]+\z/)
|
|
33
|
-
row[:active_vlans_summary] = rest.join
|
|
34
|
+
row[:active_vlans_summary] = rest.join("")
|
|
34
35
|
else
|
|
35
36
|
unparsed_records(number)
|
|
36
37
|
next
|
|
37
38
|
end
|
|
38
39
|
values[section] << row
|
|
39
|
-
elsif section
|
|
40
|
+
elsif section == :members && line.match?(/\AApplicable\s+Performed\z/) &&
|
|
41
|
+
values[:members].last&.values_at(:consistency, :reason) == [ "Not", "Consistency Check Not" ]
|
|
42
|
+
# NX-OS 将两个相邻列分别换行;只补齐已确认的同一成员。
|
|
43
|
+
values[:members].last.merge!(consistency: "Not Applicable", reason: "Consistency Check Not Performed")
|
|
44
|
+
elsif section && values[section].any? && line.match?(/\A[\d, .-]+\z/)
|
|
40
45
|
values[section].last[:active_vlans_summary] += line
|
|
41
46
|
elsif line == "Legend:"
|
|
42
47
|
next
|
|
@@ -63,6 +68,7 @@ module Vxlan
|
|
|
63
68
|
return unless @header
|
|
64
69
|
missing = %i[domain_id peer_status keepalive_status consistency_status] - values.keys
|
|
65
70
|
@issues << { code: :missing_vpc_fields, fields: missing } if missing.any?
|
|
71
|
+
@issues << { code: :incomplete_vpc_member } if values[:members].any? { |row| row[:consistency] == "Not" }
|
|
66
72
|
values.values_at(:peer_links, :members).flatten.each { |row| row[:active_vlans_truncated] = row[:active_vlans_summary].include?("...") }
|
|
67
73
|
count = values[:attributes]["Number of vPCs configured"]
|
|
68
74
|
if count
|
|
@@ -7,6 +7,22 @@ module Vxlan
|
|
|
7
7
|
module Policies
|
|
8
8
|
private
|
|
9
9
|
|
|
10
|
+
# 地址组是 ACL 语义的一部分;未知成员不能当成空组或完整配置。
|
|
11
|
+
def address_group(block)
|
|
12
|
+
sequences, members = {}, []
|
|
13
|
+
analyze_children(block) do |child|
|
|
14
|
+
match = /\A(\d+) (host )?(\S+)\z/.match(child.text)
|
|
15
|
+
next issue(child) unless match
|
|
16
|
+
sequence = integer(match[1], 1..4_294_967_295)
|
|
17
|
+
raise ArgumentError if sequences.key?(sequence)
|
|
18
|
+
sequences[sequence] = true
|
|
19
|
+
value = match[3]
|
|
20
|
+
value = "#{ip_address(value)}/32" if match[2]
|
|
21
|
+
members << network_prefix(value, family: "ipv4")
|
|
22
|
+
end
|
|
23
|
+
members.uniq.sort
|
|
24
|
+
end
|
|
25
|
+
|
|
10
26
|
# 解析有序 IP 前缀列表条目及匹配边界。
|
|
11
27
|
def prefix_list(block, lists, family:)
|
|
12
28
|
return ignore(block) if /\A(?:ip|ipv6) prefix-list \S+ description .+\z/.match?(block.text)
|