netstack 0.1.1 → 0.2.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 +22 -3
- data/{LICENSE.txt → LICENSE} +7 -6
- data/NOTICE +25 -0
- data/README.md +58 -20
- data/docs/acceptance.md +92 -0
- data/docs/backups.md +100 -0
- data/docs/configuration.md +198 -0
- data/docs/connector.md +187 -0
- data/docs/ddi.md +142 -0
- data/docs/deployment.md +47 -0
- data/docs/foundations.md +45 -0
- data/docs/implementation.md +68 -0
- data/docs/inventory.md +42 -0
- data/docs/monitoring.md +227 -0
- data/docs/motor_integration.md +96 -0
- data/docs/performance.md +39 -0
- data/docs/release.md +20 -0
- data/docs/vxlan.md +91 -0
- data/docs/wireless.md +81 -0
- data/lib/netstack/backups/artifact.rb +23 -0
- data/lib/netstack/backups/diff.rb +15 -0
- data/lib/netstack/backups/repository.rb +77 -0
- data/lib/netstack/backups/request.rb +78 -0
- data/lib/netstack/circuits/business_calendar.rb +51 -0
- data/lib/netstack/circuits/capacity_review.rb +191 -0
- data/lib/netstack/circuits/endpoint.rb +25 -0
- data/lib/netstack/circuits/monitoring_plan.rb +212 -0
- data/lib/netstack/configuration/artifact.rb +85 -0
- data/lib/netstack/configuration/artifact_resolver.rb +30 -0
- data/lib/netstack/configuration/bootstrap/payload.rb +67 -0
- data/lib/netstack/configuration/bootstrap/validator.rb +224 -0
- data/lib/netstack/configuration/catalog.rb +60 -0
- data/lib/netstack/configuration/generator.rb +87 -0
- data/lib/netstack/configuration/input_contract.rb +82 -0
- data/lib/netstack/configuration/payload.rb +168 -0
- data/lib/netstack/configuration/reader.rb +124 -0
- data/lib/netstack/configuration/support.rb +30 -0
- data/lib/netstack/configuration/system_configuration.rb +56 -0
- data/lib/netstack/configuration/template_manager.rb +174 -0
- data/lib/netstack/configuration/template_store.rb +40 -0
- data/lib/netstack/configuration/validator.rb +566 -0
- data/lib/netstack/connector/authentication.rb +77 -0
- data/lib/netstack/connector/authentication_error.rb +11 -0
- data/lib/netstack/connector/backup.rb +19 -0
- data/lib/netstack/connector/backup_busy.rb +11 -0
- data/lib/netstack/connector/backup_persistence_error.rb +32 -0
- data/lib/netstack/connector/command.rb +63 -0
- data/lib/netstack/connector/command_result.rb +24 -0
- data/lib/netstack/connector/command_timeout.rb +11 -0
- data/lib/netstack/connector/configuration.rb +138 -0
- data/lib/netstack/connector/connection_closed.rb +11 -0
- data/lib/netstack/connector/connection_error.rb +11 -0
- data/lib/netstack/connector/device.rb +284 -0
- data/lib/netstack/connector/device_error.rb +11 -0
- data/lib/netstack/connector/dialogue.rb +55 -0
- data/lib/netstack/connector/error.rb +44 -0
- data/lib/netstack/connector/error_metadata.rb +58 -0
- data/lib/netstack/connector/event.rb +24 -0
- data/lib/netstack/connector/execution.rb +110 -0
- data/lib/netstack/connector/interaction.rb +55 -0
- data/lib/netstack/connector/internal_error.rb +11 -0
- data/lib/netstack/connector/known_hosts.rb +128 -0
- data/lib/netstack/connector/local_backup.rb +63 -0
- data/lib/netstack/connector/log/event.rb +65 -0
- data/lib/netstack/connector/log/formatter.rb +18 -0
- data/lib/netstack/connector/log/messages.rb +54 -0
- data/lib/netstack/connector/log/redacting_writer.rb +33 -0
- data/lib/netstack/connector/log/transcript.rb +35 -0
- data/lib/netstack/connector/log.rb +262 -0
- data/lib/netstack/connector/log_error.rb +11 -0
- data/lib/netstack/connector/login_timeout.rb +11 -0
- data/lib/netstack/connector/output_limit_exceeded.rb +11 -0
- data/lib/netstack/connector/parsing_error.rb +11 -0
- data/lib/netstack/connector/profile/builder.rb +238 -0
- data/lib/netstack/connector/profile.rb +225 -0
- data/lib/netstack/connector/prompt_error.rb +11 -0
- data/lib/netstack/connector/recovery.rb +59 -0
- data/lib/netstack/connector/redactor.rb +100 -0
- data/lib/netstack/connector/response.rb +25 -0
- data/lib/netstack/connector/response_reader.rb +140 -0
- data/lib/netstack/connector/result.rb +50 -0
- data/lib/netstack/connector/running_config/rendered.rb +16 -0
- data/lib/netstack/connector/running_config/strategy.rb +28 -0
- data/lib/netstack/connector/running_config.rb +113 -0
- data/lib/netstack/connector/save_config.rb +23 -0
- data/lib/netstack/connector/script.rb +59 -0
- data/lib/netstack/connector/script_error.rb +11 -0
- data/lib/netstack/connector/script_output_limit_exceeded.rb +11 -0
- data/lib/netstack/connector/session.rb +414 -0
- data/lib/netstack/connector/session_busy.rb +11 -0
- data/lib/netstack/connector/storage/backup_lock.rb +117 -0
- data/lib/netstack/connector/storage/private_file.rb +111 -0
- data/lib/netstack/connector/storage/safe_file.rb +64 -0
- data/lib/netstack/connector/storage/saved_config.rb +76 -0
- data/lib/netstack/connector/terminal_renderer.rb +186 -0
- data/lib/netstack/connector/terminal_text.rb +32 -0
- data/lib/netstack/connector/textfsm.rb +71 -0
- data/lib/netstack/connector/tftp/file_upload.rb +53 -0
- data/lib/netstack/connector/tftp/strategy.rb +67 -0
- data/lib/netstack/connector/tftp.rb +116 -0
- data/lib/netstack/connector/tftp_completion_error.rb +36 -0
- data/lib/netstack/connector/tftp_receipt.rb +54 -0
- data/lib/netstack/connector/tftp_target.rb +65 -0
- data/lib/netstack/connector/topology/checkpoint_error.rb +16 -0
- data/lib/netstack/connector/topology/deployment_adapter.rb +163 -0
- data/lib/netstack/connector/topology/execution_result.rb +34 -0
- data/lib/netstack/connector/topology/immediate_strategy.rb +39 -0
- data/lib/netstack/connector/topology/interface_description.rb +46 -0
- data/lib/netstack/connector/topology/interface_name.rb +60 -0
- data/lib/netstack/connector/topology/plan.rb +105 -0
- data/lib/netstack/connector/topology/strategy.rb +76 -0
- data/lib/netstack/connector/topology.rb +255 -0
- data/lib/netstack/connector/transport_error.rb +11 -0
- data/lib/netstack/connector/transports.rb +201 -0
- data/lib/netstack/connector/underlying_error.rb +25 -0
- data/lib/netstack/connector/unsupported_operation.rb +11 -0
- data/lib/netstack/connector/vendors/cisco_ios/running_config.rb +22 -0
- data/lib/netstack/connector/vendors/cisco_ios/tftp_backup.rb +41 -0
- data/lib/netstack/connector/vendors/cisco_ios/topology.rb +54 -0
- data/lib/netstack/connector/vendors/cisco_ios.rb +44 -0
- data/lib/netstack/connector/vendors/cisco_nxos/running_config.rb +25 -0
- data/lib/netstack/connector/vendors/cisco_nxos/tftp_backup.rb +43 -0
- data/lib/netstack/connector/vendors/cisco_nxos.rb +51 -0
- data/lib/netstack/connector/vendors/h3c/tftp_backup.rb +34 -0
- data/lib/netstack/connector/vendors/h3c/topology.rb +72 -0
- data/lib/netstack/connector/vendors/h3c.rb +75 -0
- data/lib/netstack/connector/vendors/h3c_wireless.rb +13 -0
- data/lib/netstack/connector/vendors/hillstone/running_config.rb +16 -0
- data/lib/netstack/connector/vendors/hillstone/tftp_backup.rb +55 -0
- data/lib/netstack/connector/vendors/hillstone/topology.rb +64 -0
- data/lib/netstack/connector/vendors/hillstone.rb +39 -0
- data/lib/netstack/connector/vendors/huawei/tftp_backup.rb +26 -0
- data/lib/netstack/connector/vendors/huawei.rb +62 -0
- data/lib/netstack/connector/vendors/palo_alto/running_config.rb +77 -0
- data/lib/netstack/connector/vendors/palo_alto/tftp_backup.rb +49 -0
- data/lib/netstack/connector/vendors/palo_alto/topology.rb +77 -0
- data/lib/netstack/connector/vendors/palo_alto.rb +39 -0
- data/lib/netstack/connector/vendors/radware/running_config.rb +21 -0
- data/lib/netstack/connector/vendors/radware/tftp_backup.rb +49 -0
- data/lib/netstack/connector/vendors/radware/topology.rb +19 -0
- data/lib/netstack/connector/vendors/radware.rb +59 -0
- data/lib/netstack/connector/write_timeout.rb +11 -0
- data/lib/netstack/connector.rb +32 -0
- data/lib/netstack/ddi/applier.rb +107 -0
- data/lib/netstack/ddi/deployment.rb +137 -0
- data/lib/netstack/ddi/inputs.rb +61 -0
- data/lib/netstack/ddi/manager.rb +197 -0
- data/lib/netstack/ddi/plan.rb +99 -0
- data/lib/netstack/ddi/scope.rb +127 -0
- data/lib/netstack/ddi/settings.rb +44 -0
- data/lib/netstack/ddi/state.rb +166 -0
- data/lib/netstack/deployment/checkpoint_error.rb +14 -0
- data/lib/netstack/deployment/cli_adapter.rb +64 -0
- data/lib/netstack/deployment/executor.rb +131 -0
- data/lib/netstack/deployment/plan.rb +82 -0
- data/lib/netstack/deployment/receipt.rb +78 -0
- data/lib/netstack/deployment/recovery.rb +19 -0
- data/lib/netstack/deployment/step.rb +16 -0
- data/lib/netstack/dimension.rb +30 -0
- data/lib/netstack/error.rb +14 -0
- data/lib/netstack/input_error.rb +9 -0
- data/lib/netstack/integrations/bluecat/client.rb +84 -0
- data/lib/netstack/integrations/http/client.rb +146 -0
- data/lib/netstack/integrations/http/error.rb +10 -0
- data/lib/netstack/integrations/http/response.rb +28 -0
- data/lib/netstack/integrations/http_delivery/adapter.rb +51 -0
- data/lib/netstack/integrations/http_delivery/client.rb +127 -0
- data/lib/netstack/integrations/http_delivery/result.rb +27 -0
- data/lib/netstack/integrations/oxidized/client.rb +89 -0
- data/lib/netstack/integrations/zabbix/client.rb +211 -0
- data/lib/netstack/integrations/zabbix/error.rb +10 -0
- data/lib/netstack/inventory/address.rb +30 -0
- data/lib/netstack/inventory/changes.rb +13 -0
- data/lib/netstack/inventory/cli_parser.rb +75 -0
- data/lib/netstack/inventory/collector.rb +99 -0
- data/lib/netstack/inventory/device.rb +14 -0
- data/lib/netstack/inventory/interface.rb +25 -0
- data/lib/netstack/inventory/neighbor.rb +15 -0
- data/lib/netstack/inventory/reconciler.rb +74 -0
- data/lib/netstack/inventory/snapshot.rb +69 -0
- data/lib/netstack/inventory/transceiver.rb +23 -0
- data/lib/netstack/ipv4.rb +204 -0
- data/lib/netstack/ipv6.rb +88 -0
- data/lib/netstack/monitoring/applier.rb +179 -0
- data/lib/netstack/monitoring/device_plan.rb +23 -0
- data/lib/netstack/monitoring/host_plan.rb +152 -0
- data/lib/netstack/monitoring/inputs.rb +73 -0
- data/lib/netstack/monitoring/measurements.rb +89 -0
- data/lib/netstack/monitoring/mutation.rb +37 -0
- data/lib/netstack/monitoring/plan.rb +69 -0
- data/lib/netstack/monitoring/probe_plan.rb +19 -0
- data/lib/netstack/monitoring/state_comparison.rb +37 -0
- data/lib/netstack/range_set.rb +181 -0
- data/lib/netstack/routing/lookup.rb +46 -0
- data/lib/netstack/routing/lookup_result.rb +14 -0
- data/lib/netstack/routing/route.rb +32 -0
- data/lib/netstack/routing/snapshot.rb +23 -0
- data/lib/netstack/target.rb +15 -0
- data/lib/netstack/unsupported_capability.rb +9 -0
- data/lib/netstack/values.rb +70 -0
- data/lib/netstack/version.rb +1 -1
- data/lib/netstack/vxlan/auditor.rb +187 -0
- data/lib/netstack/vxlan/cisco_nxos/config_parser.rb +327 -0
- data/lib/netstack/vxlan/cisco_nxos/deployment_adapter.rb +63 -0
- data/lib/netstack/vxlan/cisco_nxos/runtime_parser.rb +182 -0
- data/lib/netstack/vxlan/cisco_nxos/segment_renderer.rb +42 -0
- data/lib/netstack/vxlan/collector.rb +77 -0
- data/lib/netstack/vxlan/fabric.rb +29 -0
- data/lib/netstack/vxlan/finding.rb +20 -0
- data/lib/netstack/vxlan/node.rb +51 -0
- data/lib/netstack/vxlan/route_policy.rb +57 -0
- data/lib/netstack/vxlan/runtime_result.rb +24 -0
- data/lib/netstack/vxlan/segment.rb +35 -0
- data/lib/netstack/vxlan/segment_plan.rb +25 -0
- data/lib/netstack/vxlan/segment_planner.rb +156 -0
- data/lib/netstack/vxlan/snapshot.rb +34 -0
- data/lib/netstack/vxlan/validation.rb +58 -0
- data/lib/netstack/vxlan/verifier.rb +52 -0
- data/lib/netstack/vxlan/vpc_domain.rb +20 -0
- data/lib/netstack/wireless/access_point.rb +12 -0
- data/lib/netstack/wireless/ap_group.rb +12 -0
- data/lib/netstack/wireless/assessment.rb +13 -0
- data/lib/netstack/wireless/assessor.rb +83 -0
- data/lib/netstack/wireless/authentication_connection.rb +12 -0
- data/lib/netstack/wireless/bss.rb +12 -0
- data/lib/netstack/wireless/client.rb +12 -0
- data/lib/netstack/wireless/collector.rb +191 -0
- data/lib/netstack/wireless/configuration.rb +13 -0
- data/lib/netstack/wireless/finding.rb +12 -0
- data/lib/netstack/wireless/h3c/config_parser.rb +229 -0
- data/lib/netstack/wireless/h3c/lldp_parser.rb +69 -0
- data/lib/netstack/wireless/h3c/runtime_parser.rb +240 -0
- data/lib/netstack/wireless/huawei/runtime_parser.rb +83 -0
- data/lib/netstack/wireless/invalid_snapshot.rb +11 -0
- data/lib/netstack/wireless/neighbor.rb +12 -0
- data/lib/netstack/wireless/normalizer.rb +90 -0
- data/lib/netstack/wireless/parse_result.rb +13 -0
- data/lib/netstack/wireless/radio.rb +12 -0
- data/lib/netstack/wireless/reconciler.rb +115 -0
- data/lib/netstack/wireless/reconciliation.rb +11 -0
- data/lib/netstack/wireless/security_policy.rb +12 -0
- data/lib/netstack/wireless/service_template.rb +12 -0
- data/lib/netstack/wireless/service_template_bindings.rb +56 -0
- data/lib/netstack/wireless/snapshot.rb +150 -0
- data/lib/netstack/wireless/state.rb +23 -0
- data/lib/netstack/wireless/view.rb +154 -0
- data/lib/netstack.rb +10 -3
- data/resources/configuration/cisco-bootstrap.erb +118 -0
- data/resources/configuration/cisco-distributed-gateway.erb +40 -0
- data/resources/configuration/cisco-n9k-vpc.erb +62 -0
- data/resources/configuration/cisco-server-port.erb +43 -0
- data/resources/configuration/h3c-bootstrap.erb +108 -0
- data/resources/configuration/h3c-irf.erb +57 -0
- data/resources/configuration/h3c-server-port.erb +36 -0
- data/resources/configuration/hillstone-ipsec-vpn.erb +68 -0
- data/resources/configuration/hillstone-policy.erb +31 -0
- data/resources/configuration/paloalto-ipsec-vpn.erb +39 -0
- data/resources/configuration/radware-load-balancer.erb +26 -0
- data/resources/textfsm/cisco_cdp_neighbors_detail.textfsm +7 -0
- data/resources/textfsm/cisco_ios_running_config_interfaces.textfsm +12 -0
- data/resources/textfsm/cisco_ios_show_ip_interface_brief.textfsm +7 -0
- data/resources/textfsm/h3c_interface_descriptions.textfsm +11 -0
- data/resources/textfsm/h3c_lldp_local_first.textfsm +8 -0
- data/resources/textfsm/h3c_lldp_name_first.textfsm +8 -0
- data/resources/textfsm/hillstone_interface_descriptions.textfsm +11 -0
- data/resources/textfsm/hillstone_lldp_neighbors.textfsm +7 -0
- data/resources/textfsm/index +8 -0
- data/resources/textfsm/palo_alto_interface_descriptions.textfsm +6 -0
- data/resources/textfsm/palo_alto_lldp_neighbors.textfsm +11 -0
- data/resources/textfsm/radware_port_names.textfsm +13 -0
- metadata +588 -27
- data/.rubocop.yml +0 -13
- data/CODE_OF_CONDUCT.md +0 -84
- data/Gemfile +0 -10
- data/Gemfile.lock +0 -40
- data/Rakefile +0 -8
- data/bin/console +0 -15
- data/bin/setup +0 -8
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Monitoring
|
|
5
|
+
# Hashes are projections; arrays are unordered collections with multiplicity.
|
|
6
|
+
# Each expected entry needs its own actual entry, including overlapping projections.
|
|
7
|
+
module StateComparison
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def matches?(actual, expected)
|
|
11
|
+
case expected
|
|
12
|
+
when Hash
|
|
13
|
+
actual.is_a?(Hash) && expected.all? { |key, value| actual.key?(key) && matches?(actual[key], value) }
|
|
14
|
+
when Array
|
|
15
|
+
return false unless actual.is_a?(Array) && actual.size == expected.size
|
|
16
|
+
assigned = {}
|
|
17
|
+
expected.each_index.all? { |index| match_entry?(index, actual, expected, assigned, {}) }
|
|
18
|
+
else
|
|
19
|
+
actual == expected || ((expected.is_a?(Integer) || expected.is_a?(Float)) && actual.is_a?(String) && actual == expected.to_s)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def match_entry?(index, actual, expected, assigned, visited)
|
|
24
|
+
actual.each_index do |candidate|
|
|
25
|
+
next if visited[candidate] || !matches?(actual[candidate], expected[index])
|
|
26
|
+
visited[candidate] = true
|
|
27
|
+
if !assigned.key?(candidate) || match_entry?(assigned[candidate], actual, expected, assigned, visited)
|
|
28
|
+
assigned[candidate] = index
|
|
29
|
+
return true
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
false
|
|
33
|
+
end
|
|
34
|
+
private_class_method :match_entry?
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
# 不变量是区间有序、互不相交且已合并;遍历区间而不是逐个枚举主机。
|
|
5
|
+
class RangeSet
|
|
6
|
+
include Enumerable
|
|
7
|
+
|
|
8
|
+
attr_reader :ranges
|
|
9
|
+
|
|
10
|
+
# 校验、排序并合并区间,然后冻结集合以保证值语义。
|
|
11
|
+
def initialize(*ranges)
|
|
12
|
+
ordered = ranges.map do |range|
|
|
13
|
+
unless range.is_a?(Range) && range.begin.is_a?(Integer) && range.end.is_a?(Integer)
|
|
14
|
+
raise ArgumentError, "Intervals must have integer endpoints"
|
|
15
|
+
end
|
|
16
|
+
first, last = range.begin, range.end
|
|
17
|
+
last -= 1 if range.exclude_end?
|
|
18
|
+
raise ArgumentError, "Intervals must be nonnegative and ordered" if first.negative? || first > last
|
|
19
|
+
[ first, last ]
|
|
20
|
+
end.sort_by(&:first)
|
|
21
|
+
merged = []
|
|
22
|
+
ordered.each do |first, last|
|
|
23
|
+
if !merged.empty? && first <= merged.last[1] + 1
|
|
24
|
+
merged.last[1] = [ merged.last[1], last ].max
|
|
25
|
+
else
|
|
26
|
+
merged << [ first, last ]
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
@ranges = merged.map { |first, last| first..last }.freeze
|
|
30
|
+
@hash = @ranges.hash
|
|
31
|
+
freeze
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# 遍历规范化后的区间;没有代码块时返回枚举器。
|
|
35
|
+
def each(&block)
|
|
36
|
+
return enum_for(__method__) unless block
|
|
37
|
+
ranges.each(&block)
|
|
38
|
+
self
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# 判断集合是否没有区间。
|
|
42
|
+
def empty? = ranges.empty?
|
|
43
|
+
|
|
44
|
+
# 返回区间段数量,而不是地址或端口总数。
|
|
45
|
+
def size = ranges.size
|
|
46
|
+
|
|
47
|
+
# 返回最小端点;空集合返回 nil。
|
|
48
|
+
def min = ranges.first&.begin
|
|
49
|
+
|
|
50
|
+
# 返回最大端点;空集合返回 nil。
|
|
51
|
+
def max = ranges.last&.end
|
|
52
|
+
|
|
53
|
+
# 计算所有闭区间包含的元素总数。
|
|
54
|
+
def cardinality = sum { |range| range.end - range.begin + 1 }
|
|
55
|
+
|
|
56
|
+
attr_reader :hash
|
|
57
|
+
# 仅按规范化后的区间比较集合内容。
|
|
58
|
+
def ==(other) = other.is_a?(RangeSet) && ranges == other.ranges
|
|
59
|
+
|
|
60
|
+
alias_method :eql?, :==
|
|
61
|
+
|
|
62
|
+
# 计算并集;两个集合为空或相同对象时直接复用已有值。
|
|
63
|
+
def |(other)
|
|
64
|
+
validate_operand!(other)
|
|
65
|
+
return self if other.empty? || equal?(other)
|
|
66
|
+
return other if empty?
|
|
67
|
+
self.class.new(*ranges, *other.ranges)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# 用双指针扫描两个有序集合,计算交集。
|
|
71
|
+
def &(other)
|
|
72
|
+
validate_operand!(other)
|
|
73
|
+
return self if equal?(other) || empty?
|
|
74
|
+
return other if other.empty?
|
|
75
|
+
|
|
76
|
+
result = []
|
|
77
|
+
left = right = 0
|
|
78
|
+
while left < size && right < other.size
|
|
79
|
+
a, b = ranges[left], other.ranges[right]
|
|
80
|
+
first, last = [ a.begin, b.begin ].max, [ a.end, b.end ].min
|
|
81
|
+
result << (first..last) if first <= last
|
|
82
|
+
left += 1 if a.end <= b.end
|
|
83
|
+
right += 1 if b.end <= a.end
|
|
84
|
+
end
|
|
85
|
+
self.class.new(*result)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# 从当前集合中减去另一个集合,逐段截去重叠部分。
|
|
89
|
+
def -(other)
|
|
90
|
+
validate_operand!(other)
|
|
91
|
+
return self if empty? || other.empty?
|
|
92
|
+
return self.class.new if equal?(other)
|
|
93
|
+
|
|
94
|
+
result = []
|
|
95
|
+
right = 0
|
|
96
|
+
each do |range|
|
|
97
|
+
cursor = range.begin
|
|
98
|
+
right += 1 while right < other.size && other.ranges[right].end < cursor
|
|
99
|
+
scan = right
|
|
100
|
+
while scan < other.size && other.ranges[scan].begin <= range.end
|
|
101
|
+
removed = other.ranges[scan]
|
|
102
|
+
result << (cursor..(removed.begin - 1)) if removed.begin > cursor
|
|
103
|
+
cursor = [ cursor, removed.end + 1 ].max
|
|
104
|
+
break if cursor > range.end
|
|
105
|
+
scan += 1
|
|
106
|
+
end
|
|
107
|
+
result << (cursor..range.end) if cursor <= range.end
|
|
108
|
+
end
|
|
109
|
+
self.class.new(*result)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# 判断整数是否落在任一规范化区间内。
|
|
113
|
+
def include?(number)
|
|
114
|
+
raise ArgumentError, "Membership requires an integer" unless number.is_a?(Integer)
|
|
115
|
+
candidate = ranges.bsearch { |range| range.end >= number }
|
|
116
|
+
candidate ? candidate.begin <= number : false
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# 判断集合是否被当前集合覆盖;空集合始终被覆盖。
|
|
120
|
+
def cover?(other)
|
|
121
|
+
return include?(other) if other.is_a?(Integer)
|
|
122
|
+
validate_operand!(other)
|
|
123
|
+
if other.size == 1
|
|
124
|
+
target = other.ranges.first
|
|
125
|
+
candidate = ranges.bsearch { |range| range.end >= target.end }
|
|
126
|
+
return candidate ? candidate.begin <= target.begin : false
|
|
127
|
+
end
|
|
128
|
+
index = 0
|
|
129
|
+
other.each do |range|
|
|
130
|
+
index += 1 while index < size && ranges[index].end < range.begin
|
|
131
|
+
return false if index == size || ranges[index].begin > range.begin || ranges[index].end < range.end
|
|
132
|
+
end
|
|
133
|
+
true
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# 判断当前集合是否是另一个集合的子集。
|
|
137
|
+
def subset?(other)
|
|
138
|
+
validate_operand!(other)
|
|
139
|
+
other.cover?(self)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# 判断两个有序集合是否存在重叠区间。
|
|
143
|
+
def overlap?(other)
|
|
144
|
+
validate_operand!(other)
|
|
145
|
+
left = right = 0
|
|
146
|
+
while left < size && right < other.size
|
|
147
|
+
a, b = ranges[left], other.ranges[right]
|
|
148
|
+
return true if a.overlap?(b)
|
|
149
|
+
(a.end < b.end) ? left += 1 : right += 1
|
|
150
|
+
end
|
|
151
|
+
false
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# 返回相等、包含、被包含或互不相同四种关系之一。
|
|
155
|
+
def relation(other)
|
|
156
|
+
validate_operand!(other)
|
|
157
|
+
return :equal if self == other
|
|
158
|
+
return :contains if cover?(other)
|
|
159
|
+
return :within if subset?(other)
|
|
160
|
+
:other
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# 返回区间数组的浅副本,避免调用方修改内部数组。
|
|
164
|
+
def to_a = ranges.dup
|
|
165
|
+
|
|
166
|
+
# 哈希值只在当前进程有效;持久化区间,并在读取进程中重新建立冻结集合。
|
|
167
|
+
def marshal_dump = ranges
|
|
168
|
+
|
|
169
|
+
# 从持久化区间恢复集合的不变量和冻结状态。
|
|
170
|
+
def marshal_load(ranges)
|
|
171
|
+
initialize(*ranges)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
private
|
|
175
|
+
|
|
176
|
+
# 统一校验集合运算的右值类型。
|
|
177
|
+
def validate_operand!(other)
|
|
178
|
+
raise ArgumentError, "Expected a RangeSet" unless other.is_a?(RangeSet)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ipaddr"
|
|
4
|
+
|
|
5
|
+
module Netstack
|
|
6
|
+
# 路由证据与本地查询;不安装路由,也不执行设备 I/O。
|
|
7
|
+
module Routing
|
|
8
|
+
# 先限定目标、VRF 和地址族,再按前缀长度与优先级筛选。
|
|
9
|
+
# 排序依据未知时保留歧义;并列的最优路由保留 ECMP 候选。
|
|
10
|
+
class Lookup
|
|
11
|
+
def call(routes:, destination:, target_key:, vrf: "default")
|
|
12
|
+
unless routes.is_a?(Array) && routes.all? { |route| route.is_a?(Route) }
|
|
13
|
+
raise InputError, "lookup routes must be Route values"
|
|
14
|
+
end
|
|
15
|
+
target_key = Values.text(target_key, name: "route target")
|
|
16
|
+
vrf = Values.text(vrf, name: "VRF")
|
|
17
|
+
address = ::IPAddr.new(Values.text(destination, name: "lookup destination"))
|
|
18
|
+
raise InputError, "lookup requires one unscoped host" if destination.match?(/[\/%]/)
|
|
19
|
+
|
|
20
|
+
candidates = routes.select do |route|
|
|
21
|
+
route.target_key == target_key && route.vrf == vrf && route.network.ipv4? == address.ipv4? && route.network.include?(address)
|
|
22
|
+
end
|
|
23
|
+
return LookupResult.new(routes: [], status: :not_found) if candidates.empty?
|
|
24
|
+
|
|
25
|
+
longest = candidates.map { |route| route.network.prefix }.max
|
|
26
|
+
candidates.select! { |route| route.network.prefix == longest }
|
|
27
|
+
if candidates.size > 1 && candidates.any? { |route| route.preference.nil? }
|
|
28
|
+
return LookupResult.new(routes: candidates, status: :ambiguous, reason: "route preference is unknown")
|
|
29
|
+
end
|
|
30
|
+
preference = candidates.filter_map(&:preference).min
|
|
31
|
+
candidates.select! { |route| route.preference == preference } if preference
|
|
32
|
+
if candidates.map(&:protocol).uniq.size > 1
|
|
33
|
+
return LookupResult.new(routes: candidates, status: :ambiguous, reason: "metrics from different protocols are not comparable")
|
|
34
|
+
end
|
|
35
|
+
if candidates.size > 1 && candidates.any? { |route| route.metric.nil? }
|
|
36
|
+
return LookupResult.new(routes: candidates, status: :ambiguous, reason: "route metric is unknown")
|
|
37
|
+
end
|
|
38
|
+
metric = candidates.filter_map(&:metric).min
|
|
39
|
+
candidates.select! { |route| route.metric == metric } if metric
|
|
40
|
+
LookupResult.new(routes: candidates, status: :matched)
|
|
41
|
+
rescue ::IPAddr::Error
|
|
42
|
+
raise InputError, "invalid lookup destination"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Routing
|
|
5
|
+
class LookupResult < Data.define(:routes, :status, :reason)
|
|
6
|
+
def initialize(routes:, status:, reason: nil)
|
|
7
|
+
super(routes: routes.dup.freeze, status: status, reason: Values.copy(reason))
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def matched? = status == :matched
|
|
11
|
+
def ambiguous? = status == :ambiguous
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ipaddr"
|
|
4
|
+
|
|
5
|
+
module Netstack
|
|
6
|
+
module Routing
|
|
7
|
+
class Route < Data.define(:target_key, :vrf, :destination, :protocol, :preference, :metric, :next_hop, :interface)
|
|
8
|
+
def initialize(target_key:, destination:, protocol:, vrf: "default", preference: nil, metric: nil, next_hop: nil, interface: nil)
|
|
9
|
+
text = Values.text(destination, name: "route destination")
|
|
10
|
+
raise InputError, "route destination cannot contain a scope identifier" if text.include?("%")
|
|
11
|
+
network = ::IPAddr.new(text)
|
|
12
|
+
if next_hop
|
|
13
|
+
hop = ::IPAddr.new(Values.text(next_hop, name: "next hop"))
|
|
14
|
+
raise InputError, "route next hop family mismatch" if hop.ipv4? != network.ipv4?
|
|
15
|
+
raise InputError, "next hop must be an unscoped host" if next_hop.match?(/[\/%]/)
|
|
16
|
+
|
|
17
|
+
next_hop = hop.to_s
|
|
18
|
+
end
|
|
19
|
+
super(target_key: Values.text(target_key, name: "route target"), vrf: Values.text(vrf, name: "VRF"),
|
|
20
|
+
destination: "#{network}/#{network.prefix}".freeze, protocol: Values.text(protocol, name: "route protocol").downcase.freeze,
|
|
21
|
+
preference: preference.nil? ? nil : Values.integer(preference, name: "route preference", min: 0),
|
|
22
|
+
metric: metric.nil? ? nil : Values.integer(metric, name: "route metric", min: 0),
|
|
23
|
+
next_hop: Values.copy(next_hop), interface: Values.text(interface, name: "route interface", optional: true))
|
|
24
|
+
rescue ::IPAddr::Error
|
|
25
|
+
raise InputError, "invalid route address"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def network = ::IPAddr.new(destination)
|
|
29
|
+
def key = [target_key, vrf, destination, protocol, preference, metric, next_hop, interface].freeze
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Routing
|
|
5
|
+
class Snapshot < Data.define(:target, :source, :vrf, :dimension)
|
|
6
|
+
def initialize(target:, source:, vrf:, dimension:)
|
|
7
|
+
unless target.is_a?(Target) && dimension.is_a?(Dimension) && dimension.name == :routes
|
|
8
|
+
raise InputError, "invalid route snapshot"
|
|
9
|
+
end
|
|
10
|
+
vrf = Values.text(vrf, name: "VRF")
|
|
11
|
+
unless dimension.records.all? { |route| route.is_a?(Route) && route.target_key == target.key && route.vrf == vrf }
|
|
12
|
+
raise InputError, "route snapshot contains foreign routes"
|
|
13
|
+
end
|
|
14
|
+
identities = dimension.records.map(&:key)
|
|
15
|
+
raise InputError, "duplicate route identity" unless identities.uniq.size == identities.size
|
|
16
|
+
|
|
17
|
+
super(target: target, source: Values.text(source, name: "route source"), vrf: vrf, dimension: dimension)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def complete? = dimension.complete?
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
# 宿主提供的不可变目标身份;授权和凭据由宿主另外管理。
|
|
5
|
+
# 属性会进入计划,宿主必须确保其中不包含秘密。
|
|
6
|
+
class Target < Data.define(:key, :host, :platform, :attributes)
|
|
7
|
+
def initialize(key:, host:, platform:, attributes: {})
|
|
8
|
+
raise InputError, "attributes must be a Hash" unless attributes.is_a?(Hash)
|
|
9
|
+
|
|
10
|
+
super(key: Values.text(key, name: "target key"), host: Values.text(host, name: "host"),
|
|
11
|
+
platform: Values.text(platform.is_a?(Symbol) ? platform.to_s : platform, name: "platform"),
|
|
12
|
+
attributes: Values.copy(attributes))
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
require "active_support/core_ext/object/deep_dup"
|
|
5
|
+
require "active_support/core_ext/object/blank"
|
|
6
|
+
|
|
7
|
+
module Netstack
|
|
8
|
+
# 防御性复制宿主交换值;只接受明确支持的类型,并建立深度不可变约束。
|
|
9
|
+
module Values
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def copy(value)
|
|
13
|
+
freeze_value(value.deep_dup)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# ActiveSupport 负责深复制;此遍历只建立领域值的不可变契约。
|
|
17
|
+
def freeze_value(value)
|
|
18
|
+
case value
|
|
19
|
+
when Hash
|
|
20
|
+
value.to_h { |key, item| [freeze_value(key), freeze_value(item)] }.freeze
|
|
21
|
+
when Array
|
|
22
|
+
value.map! { |item| freeze_value(item) }.freeze
|
|
23
|
+
when String, Time
|
|
24
|
+
value.freeze
|
|
25
|
+
when NilClass, TrueClass, FalseClass, Integer, Symbol
|
|
26
|
+
value
|
|
27
|
+
when Float
|
|
28
|
+
raise InputError, "numeric values must be finite" unless value.finite?
|
|
29
|
+
|
|
30
|
+
value
|
|
31
|
+
when Data
|
|
32
|
+
value.class.new(**value.to_h.transform_values { |item| copy(item) })
|
|
33
|
+
else
|
|
34
|
+
raise ArgumentError, "unsupported value type: #{value.class}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
private_class_method :freeze_value
|
|
38
|
+
|
|
39
|
+
# 用于身份与标签的文本归一化,会去除首尾空白;不能用于不透明凭据。
|
|
40
|
+
def text(value, name:, optional: false)
|
|
41
|
+
return nil if optional && value.nil?
|
|
42
|
+
raise InputError, "#{name} must be nonempty text" unless value.is_a?(String) && value.present?
|
|
43
|
+
|
|
44
|
+
value.strip.freeze
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def integer(value, name:, min: nil, max: nil)
|
|
48
|
+
parsed = if value.is_a?(Integer)
|
|
49
|
+
value
|
|
50
|
+
elsif value.is_a?(String) && /\A[0-9]+\z/.match?(value) && value.length <= 20
|
|
51
|
+
Integer(value, 10)
|
|
52
|
+
end
|
|
53
|
+
unless parsed && (!min || parsed >= min) && (!max || parsed <= max)
|
|
54
|
+
raise InputError, "#{name} must be an integer in range"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
parsed
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def time(value, name:, optional: false)
|
|
61
|
+
return nil if optional && value.nil?
|
|
62
|
+
return value.dup.freeze if value.is_a?(Time)
|
|
63
|
+
raise InputError, "#{name} must be an ISO 8601 timestamp" unless value.is_a?(String)
|
|
64
|
+
|
|
65
|
+
Time.iso8601(value).freeze
|
|
66
|
+
rescue ArgumentError
|
|
67
|
+
raise InputError, "#{name} must be an ISO 8601 timestamp"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
data/lib/netstack/version.rb
CHANGED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Vxlan
|
|
5
|
+
class Auditor
|
|
6
|
+
def inspect(snapshot)
|
|
7
|
+
findings = []
|
|
8
|
+
fabric = snapshot.fabric
|
|
9
|
+
(fabric.expected_node_keys - fabric.nodes.map(&:key)).each do |key|
|
|
10
|
+
add(findings, :configuration_incomplete, :warning, [key], "Configuration evidence is unavailable")
|
|
11
|
+
inspect_runtime(Node.new(key: key), snapshot.runtime.fetch(key, {}), findings)
|
|
12
|
+
end
|
|
13
|
+
fabric.nodes.each do |node|
|
|
14
|
+
add(findings, :configuration_incomplete, :warning, [node.key], "Configuration evidence is incomplete") unless node.complete?
|
|
15
|
+
inspect_node(node, findings)
|
|
16
|
+
inspect_runtime(node, snapshot.runtime.fetch(node.key, {}), findings)
|
|
17
|
+
end
|
|
18
|
+
inspect_consistency(fabric, findings)
|
|
19
|
+
inspect_vpc(fabric, findings)
|
|
20
|
+
findings.freeze
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def add(findings, code, severity, keys, message, **details)
|
|
26
|
+
findings << Finding.new(code: code, severity: severity, node_keys: keys, message: message, details: details)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def inspect_node(node, findings)
|
|
30
|
+
inspect_bgp(node, findings) if node.complete?
|
|
31
|
+
return unless node.vtep?
|
|
32
|
+
|
|
33
|
+
add(findings, :nve_shutdown, :critical, [node.key], "NVE interface is administratively down") if node.nve[:shutdown]
|
|
34
|
+
return unless node.complete?
|
|
35
|
+
|
|
36
|
+
add(findings, :nve_source_missing, :critical, [node.key], "NVE source interface is missing") unless node.nve[:source_interface]
|
|
37
|
+
node.segments.each do |segment|
|
|
38
|
+
details = { vlan_id: segment.vlan_id, vni: segment.vni, vrf: segment.vrf, prefix: segment.prefix }
|
|
39
|
+
add(findings, :anycast_gateway_disabled, :warning, [node.key], "SVI does not enable anycast gateway", **details) unless segment.anycast
|
|
40
|
+
add(findings, :anycast_mac_missing, :critical, [node.key], "Global anycast gateway MAC is missing", **details) if segment.anycast && !node.anycast_mac
|
|
41
|
+
member = node.nve.fetch(:members, {})[segment.vni]
|
|
42
|
+
add(findings, :nve_member_missing, :critical, [node.key], "Segment VNI is missing from NVE", **details) unless member
|
|
43
|
+
if member && (member[:associate_vrf] || (!member[:replication] && !member[:multicast_group]))
|
|
44
|
+
add(findings, :nve_member_invalid, :critical, [node.key], "L2 NVE member has invalid replication or L3 association", **details)
|
|
45
|
+
end
|
|
46
|
+
evpn = node.evpn[segment.vni]
|
|
47
|
+
add(findings, :evpn_vni_missing, :critical, [node.key], "L2 VNI is missing from EVPN", **details) unless evpn
|
|
48
|
+
if evpn && !bidirectional?(evpn[:route_targets])
|
|
49
|
+
add(findings, :evpn_route_target_missing, :critical, [node.key], "EVPN import/export route-target is incomplete", **details)
|
|
50
|
+
end
|
|
51
|
+
vrf = node.vrfs[segment.vrf]
|
|
52
|
+
unless vrf && vrf[:vni] && node.nve.fetch(:members, {}).dig(vrf[:vni], :associate_vrf)
|
|
53
|
+
add(findings, :vrf_l3_vni_missing, :critical, [node.key], "VRF L3 VNI or NVE association is missing", **details)
|
|
54
|
+
end
|
|
55
|
+
if vrf && !bidirectional?(vrf[:route_targets].select { |target| target[:evpn] })
|
|
56
|
+
add(findings, :vrf_route_target_missing, :critical, [node.key], "VRF EVPN import/export route-target is incomplete", **details)
|
|
57
|
+
end
|
|
58
|
+
advertisement = RoutePolicy.new.advertisement(node, segment)
|
|
59
|
+
unless advertisement == :permitted
|
|
60
|
+
add(findings, :route_advertisement, advertisement == :unknown ? :warning : :critical, [node.key],
|
|
61
|
+
"Segment route advertisement is #{advertisement}", **details, decision: advertisement)
|
|
62
|
+
end
|
|
63
|
+
segment.acl_bindings.each do |direction, name|
|
|
64
|
+
unless node.acls.key?(name)
|
|
65
|
+
add(findings, :acl_missing, :critical, [node.key], "SVI references an absent ACL", **details, acl: name,
|
|
66
|
+
direction: direction)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
node.vlan_vnis.each do |vlan_id, vni|
|
|
71
|
+
next unless vni && !node.segment(vlan_id)
|
|
72
|
+
next if node.vrfs.values.any? { |vrf| vrf[:vni] == vni }
|
|
73
|
+
|
|
74
|
+
add(findings, :unrouted_vlan, :warning, [node.key], "Mapped VLAN has no parsed routed SVI", vlan_id: vlan_id, vni: vni)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def bidirectional?(targets)
|
|
79
|
+
directions = targets.flat_map { |target| target[:direction] == :both ? %i[import export] : [target[:direction]] }
|
|
80
|
+
(%i[import export] - directions).empty?
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def inspect_bgp(node, findings)
|
|
84
|
+
node.bgp.fetch(:neighbors, {}).each do |peer, settings|
|
|
85
|
+
template = node.bgp.fetch(:templates, {})[settings[:inherit_peer]] || {}
|
|
86
|
+
families = (template[:address_families] || {}).merge(settings[:address_families]) do |_key, parent, child|
|
|
87
|
+
parent.merge(child.compact)
|
|
88
|
+
end
|
|
89
|
+
family = families["l2vpn evpn"]
|
|
90
|
+
next unless family
|
|
91
|
+
|
|
92
|
+
unless family[:extended_community]
|
|
93
|
+
add(findings, :bgp_extended_community_missing, :critical, [node.key], "EVPN neighbor lacks extended community advertisement",
|
|
94
|
+
peer_ip: peer)
|
|
95
|
+
end
|
|
96
|
+
if node.role == :spine && !family[:route_reflector_client]
|
|
97
|
+
add(findings, :bgp_reflector_client_missing, :warning, [node.key], "Spine EVPN neighbor is not a route-reflector client", peer_ip: peer)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def inspect_consistency(fabric, findings)
|
|
103
|
+
fabric.vteps.flat_map { |node| node.segments.map { |segment| [node, segment] } }.group_by { |_node, segment| segment.identity }.each do |identity, rows|
|
|
104
|
+
signatures = rows.map { |_node, segment| segment.to_h }.uniq
|
|
105
|
+
if signatures.size > 1
|
|
106
|
+
add(findings, :segment_inconsistent, :critical, rows.map do |node, _segment|
|
|
107
|
+
node.key
|
|
108
|
+
end, "Segment configuration differs between nodes", identity: identity, values: signatures)
|
|
109
|
+
end
|
|
110
|
+
acl_contents = rows.map do |node, segment|
|
|
111
|
+
segment.acl_bindings.transform_values { |name| node.acls[name] }
|
|
112
|
+
end
|
|
113
|
+
next unless acl_contents.uniq.size > 1
|
|
114
|
+
|
|
115
|
+
add(findings, :acl_inconsistent, :critical, rows.map do |node, _segment|
|
|
116
|
+
node.key
|
|
117
|
+
end, "Segment ACL contents differ between nodes", identity: identity)
|
|
118
|
+
end
|
|
119
|
+
fabric.vteps.flat_map { |node| node.segments.map { |segment| [node, segment] } }.group_by { |_node, segment| segment.vni }.each do |vni, rows|
|
|
120
|
+
identities = rows.map { |_node, segment| segment.identity }.uniq
|
|
121
|
+
next unless identities.size > 1
|
|
122
|
+
|
|
123
|
+
add(findings, :vni_conflict, :critical, rows.map do |node, _segment|
|
|
124
|
+
node.key
|
|
125
|
+
end.uniq, "VNI represents different routed segments", vni: vni, identities: identities)
|
|
126
|
+
end
|
|
127
|
+
macs = fabric.vteps.filter_map(&:anycast_mac).uniq
|
|
128
|
+
add(findings, :anycast_mac_inconsistent, :critical, fabric.vteps.map(&:key), "Anycast gateway MAC differs between VTEPs", values: macs) if macs.size > 1
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def inspect_vpc(fabric, findings)
|
|
132
|
+
fabric.nodes.select(&:vpc_domain).group_by { |node| node.vpc_domain.id }.each do |id, nodes|
|
|
133
|
+
if nodes.size != 2
|
|
134
|
+
add(findings, :vpc_member_count, :warning, nodes.map(&:key), "vPC domain does not have exactly two observed nodes", domain_id: id,
|
|
135
|
+
count: nodes.size)
|
|
136
|
+
end
|
|
137
|
+
nodes.each do |node|
|
|
138
|
+
domain = node.vpc_domain
|
|
139
|
+
if node.complete? && (domain.peer_links.empty? || domain.peer_keepalive[:destination].nil?)
|
|
140
|
+
add(findings, :vpc_peer_configuration_missing, :critical, [node.key], "vPC peer-link or keepalive destination is missing", domain_id: id)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
next unless nodes.size == 2
|
|
144
|
+
|
|
145
|
+
left, right = nodes.map(&:vpc_domain)
|
|
146
|
+
if left.peer_keepalive[:destination] != right.peer_keepalive[:source] || right.peer_keepalive[:destination] != left.peer_keepalive[:source]
|
|
147
|
+
add(findings, :vpc_keepalive_inconsistent, :critical, nodes.map(&:key), "vPC keepalive endpoints are not reciprocal", domain_id: id)
|
|
148
|
+
end
|
|
149
|
+
if left.member_ports.keys.sort != right.member_ports.keys.sort
|
|
150
|
+
add(findings, :vpc_members_inconsistent, :critical, nodes.map(&:key), "vPC member IDs differ between peers", domain_id: id)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def inspect_runtime(node, dimensions, findings)
|
|
156
|
+
dimensions.each do |name, result|
|
|
157
|
+
unless result.complete?
|
|
158
|
+
add(findings, :runtime_incomplete, :warning, [node.key], "Runtime dimension is #{result.status}", dimension: name, status: result.status)
|
|
159
|
+
end
|
|
160
|
+
result.records.each do |row|
|
|
161
|
+
abnormal = case name
|
|
162
|
+
when :nve_peers, :nve_vnis then row[:state] != :up
|
|
163
|
+
when :bgp_neighbors then row[:state] != :established
|
|
164
|
+
when :ospf_neighbors then !row[:state].start_with?("full")
|
|
165
|
+
when :port_channels then !row[:flags].include?("U")
|
|
166
|
+
when :vpc then row[:consistency_status] != "success" || !row.fetch(:peer_status, "").include?("formed ok")
|
|
167
|
+
else false
|
|
168
|
+
end
|
|
169
|
+
add(findings, :runtime_abnormal, :critical, [node.key], "Runtime #{name} is abnormal", dimension: name, record: row) if abnormal
|
|
170
|
+
end
|
|
171
|
+
next unless result.complete? && node.complete?
|
|
172
|
+
|
|
173
|
+
expected = if name == :nve_vnis
|
|
174
|
+
node.nve.fetch(:members, {}).keys
|
|
175
|
+
else
|
|
176
|
+
name == :bgp_neighbors ? node.bgp.fetch(:neighbors, {}).keys : []
|
|
177
|
+
end
|
|
178
|
+
observed = result.records.map { |row| name == :nve_vnis ? row[:vni] : row[:peer_ip] }
|
|
179
|
+
(expected - observed).each do |identity|
|
|
180
|
+
add(findings, :runtime_object_missing, :critical, [node.key], "Configured object is absent from complete runtime evidence", dimension: name,
|
|
181
|
+
identity: identity)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|