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,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
# Fabric 证据、审计与网段意图;NX-OS 语法保留在 CiscoNxos 中。
|
|
5
|
+
module Vxlan
|
|
6
|
+
# 顺序读取明确的目标,保留失败情况与预期覆盖范围。
|
|
7
|
+
# 配置完整性与运行状态维度分别作为证据记录。
|
|
8
|
+
class Collector
|
|
9
|
+
DEFAULT_DIMENSIONS = %i[configuration nve_peers nve_vnis bgp_neighbors].freeze
|
|
10
|
+
|
|
11
|
+
def initialize(connector: Connector, clock: -> { Time.now.utc })
|
|
12
|
+
@connector = connector
|
|
13
|
+
@clock = clock
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def collect(targets:, fabric_key:, connection_options: {}, dimensions: DEFAULT_DIMENSIONS)
|
|
17
|
+
unless targets.is_a?(Array) && !targets.empty? && targets.all?(Target) && targets.map(&:key).uniq.size == targets.size
|
|
18
|
+
Validation.invalid!("collection needs a nonempty Array of unique Targets")
|
|
19
|
+
end
|
|
20
|
+
supported = [:configuration, *CiscoNxos::RuntimeParser::COMMANDS.keys]
|
|
21
|
+
unless dimensions.is_a?(Array) && !dimensions.empty? && dimensions.uniq == dimensions && (dimensions - supported).empty?
|
|
22
|
+
Validation.invalid!("unknown or duplicate VXLAN dimensions")
|
|
23
|
+
end
|
|
24
|
+
Validation.invalid!("connection_options must be a Hash") unless connection_options.is_a?(Hash)
|
|
25
|
+
nodes = []
|
|
26
|
+
runtime = {}
|
|
27
|
+
collection = {}
|
|
28
|
+
targets.each do |target|
|
|
29
|
+
runtime[target.key] = {}
|
|
30
|
+
if target.platform != "cisco_nxos"
|
|
31
|
+
collection[target.key] = :unsupported
|
|
32
|
+
dimensions.reject { |dimension| dimension == :configuration }.each do |dimension|
|
|
33
|
+
runtime[target.key][dimension] = RuntimeResult.new(dimension: dimension, status: :unsupported)
|
|
34
|
+
end
|
|
35
|
+
next
|
|
36
|
+
end
|
|
37
|
+
collection[target.key] = :complete
|
|
38
|
+
begin
|
|
39
|
+
@connector.open(:cisco_nxos, **connection_options, host: target.host) do |session|
|
|
40
|
+
dimensions.each do |dimension|
|
|
41
|
+
command = dimension == :configuration ? "show running-config" : CiscoNxos::RuntimeParser::COMMANDS.fetch(dimension)
|
|
42
|
+
result = session.execute_script([command])
|
|
43
|
+
successful = result.success? && result.steps.size == 1
|
|
44
|
+
unless successful
|
|
45
|
+
collection[target.key] = :partial
|
|
46
|
+
runtime[target.key][dimension] =
|
|
47
|
+
RuntimeResult.new(dimension: dimension, status: :failed, issues: [{ code: result.error&.code || :collection_failed }])
|
|
48
|
+
next
|
|
49
|
+
end
|
|
50
|
+
output = result.steps.first.output
|
|
51
|
+
if dimension == :configuration
|
|
52
|
+
node = CiscoNxos::ConfigParser.new.parse(output, target: target, complete: true)
|
|
53
|
+
nodes << node
|
|
54
|
+
collection[target.key] = :partial unless node.complete?
|
|
55
|
+
else
|
|
56
|
+
parsed = CiscoNxos::RuntimeParser.new.parse(dimension, output, complete: true)
|
|
57
|
+
runtime[target.key][dimension] = parsed
|
|
58
|
+
collection[target.key] = :partial unless parsed.complete?
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
rescue StandardError => e
|
|
63
|
+
collection[target.key] = nodes.any? { |node| node.key == target.key } || runtime[target.key].any? ? :partial : :failed
|
|
64
|
+
dimensions.each do |dimension|
|
|
65
|
+
next if runtime[target.key].key?(dimension) || (dimension == :configuration && nodes.any? { |node| node.key == target.key })
|
|
66
|
+
|
|
67
|
+
runtime[target.key][dimension] = RuntimeResult.new(dimension: dimension, status: :failed,
|
|
68
|
+
issues: [{ code: e.respond_to?(:code) ? e.code : :collection_failed }])
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
Snapshot.new(fabric: Fabric.new(key: fabric_key, nodes: nodes, expected_node_keys: targets.map(&:key)),
|
|
73
|
+
runtime: runtime, collection: collection, observed_at: @clock.call)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Vxlan
|
|
5
|
+
class Fabric
|
|
6
|
+
attr_reader :key, :nodes, :expected_node_keys
|
|
7
|
+
|
|
8
|
+
def initialize(key:, nodes:, expected_node_keys: nil)
|
|
9
|
+
@key = Validation.name(key, "fabric key")
|
|
10
|
+
Validation.invalid!("nodes must be an Array of Node") unless nodes.is_a?(Array) && nodes.all?(Node)
|
|
11
|
+
Validation.invalid!("fabric contains duplicate node keys") unless nodes.map(&:key).uniq.size == nodes.size
|
|
12
|
+
expected_node_keys = nodes.map(&:key) if expected_node_keys.nil?
|
|
13
|
+
Validation.invalid!("expected_node_keys must be an Array") unless expected_node_keys.is_a?(Array)
|
|
14
|
+
@expected_node_keys = expected_node_keys.map { |name| Validation.name(name, "expected node key") }.freeze
|
|
15
|
+
Validation.invalid!("fabric contains duplicate expected node keys") unless @expected_node_keys.uniq.size == @expected_node_keys.size
|
|
16
|
+
Validation.invalid!("fabric contains an unexpected node") unless (nodes.map(&:key) - @expected_node_keys).empty?
|
|
17
|
+
@nodes = nodes.dup.freeze
|
|
18
|
+
freeze
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def vteps = nodes.select(&:vtep?).freeze
|
|
22
|
+
def spines = nodes.select { |node| node.role == :spine }.freeze
|
|
23
|
+
def node(key) = nodes.find { |entry| entry.key == key }
|
|
24
|
+
def complete? = !nodes.empty? && nodes.size == expected_node_keys.size && nodes.all?(&:complete?)
|
|
25
|
+
def digest = Validation.fingerprint(expected_node_keys: expected_node_keys.sort, nodes: nodes.sort_by(&:key).map { |node| [node.key, node.config_digest] })
|
|
26
|
+
def to_h = { key: key, nodes: nodes.map(&:to_h), expected_node_keys: expected_node_keys, digest: digest }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Vxlan
|
|
5
|
+
class Finding
|
|
6
|
+
attr_reader :code, :severity, :node_keys, :message, :details
|
|
7
|
+
|
|
8
|
+
def initialize(code:, severity:, message:, node_keys: [], details: {})
|
|
9
|
+
@code = code.to_sym
|
|
10
|
+
@severity = severity.to_sym
|
|
11
|
+
@message = message.dup.freeze
|
|
12
|
+
@node_keys = Values.copy(node_keys)
|
|
13
|
+
@details = Values.copy(details)
|
|
14
|
+
freeze
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_h = { code: code, severity: severity, node_keys: node_keys, message: message, details: details }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Vxlan
|
|
5
|
+
# Parsed configuration facts. Missing facts stay absent, with completeness explicit.
|
|
6
|
+
class Node
|
|
7
|
+
attr_reader :key, :hostname, :management_ip, :role, :features, :interfaces, :vlan_vnis,
|
|
8
|
+
:vrfs, :nve, :evpn, :bgp, :vpc_domain, :acls, :prefix_lists, :route_maps,
|
|
9
|
+
:segments, :anycast_mac, :issues, :complete, :config_digest
|
|
10
|
+
|
|
11
|
+
def initialize(key:, hostname: nil, management_ip: nil, role: :unknown, features: [], interfaces: {},
|
|
12
|
+
vlan_vnis: {}, vrfs: {}, nve: {}, evpn: {}, bgp: {}, vpc_domain: nil,
|
|
13
|
+
acls: {}, prefix_lists: {}, route_maps: {}, segments: [], anycast_mac: nil,
|
|
14
|
+
complete: false, issues: [], config_digest: nil)
|
|
15
|
+
@key = Validation.name(key, "node key")
|
|
16
|
+
@hostname = hostname && Values.text(hostname, name: "hostname").dup.freeze
|
|
17
|
+
@management_ip = management_ip && Validation.address(management_ip, "management_ip")
|
|
18
|
+
Validation.invalid!("unknown VXLAN node role") unless %i[leaf border spine unknown].include?(role)
|
|
19
|
+
Validation.invalid!("complete must be true or false") unless [true, false].include?(complete)
|
|
20
|
+
@role = role
|
|
21
|
+
@complete = complete
|
|
22
|
+
Validation.invalid!("vpc_domain must be a VpcDomain or nil") unless vpc_domain.nil? || vpc_domain.is_a?(VpcDomain)
|
|
23
|
+
@vpc_domain = vpc_domain
|
|
24
|
+
@anycast_mac = anycast_mac&.dup&.freeze
|
|
25
|
+
{ features: features, interfaces: interfaces, vlan_vnis: vlan_vnis, vrfs: vrfs, nve: nve,
|
|
26
|
+
evpn: evpn, bgp: bgp, acls: acls, prefix_lists: prefix_lists, route_maps: route_maps,
|
|
27
|
+
issues: issues }.each do |name, value|
|
|
28
|
+
expected = %i[features issues].include?(name) ? Array : Hash
|
|
29
|
+
Validation.invalid!("#{name} must be a #{expected}") unless value.is_a?(expected)
|
|
30
|
+
instance_variable_set("@#{name}", Values.copy(value))
|
|
31
|
+
end
|
|
32
|
+
Validation.invalid!("segments must be an Array of Segment") unless segments.is_a?(Array) && segments.all?(Segment)
|
|
33
|
+
@segments = segments.dup.freeze
|
|
34
|
+
@config_digest = (config_digest || Validation.fingerprint(configuration)).freeze
|
|
35
|
+
freeze
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def vtep? = %i[leaf border].include?(role) || !nve.empty?
|
|
39
|
+
def complete? = complete
|
|
40
|
+
def segment(vlan_id) = segments.find { |entry| entry.vlan_id == vlan_id }
|
|
41
|
+
|
|
42
|
+
def configuration
|
|
43
|
+
{ hostname: hostname, role: role, features: features, interfaces: interfaces, vlan_vnis: vlan_vnis,
|
|
44
|
+
vrfs: vrfs, nve: nve, evpn: evpn, bgp: bgp, vpc_domain: vpc_domain&.to_h, acls: acls,
|
|
45
|
+
prefix_lists: prefix_lists, route_maps: route_maps, segments: segments.map(&:to_h), anycast_mac: anycast_mac }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def to_h = { key: key, management_ip: management_ip, complete: complete, issues: issues, config_digest: config_digest, **configuration }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Vxlan
|
|
5
|
+
# Evaluates supported prefix-list/route-map predicates in device order.
|
|
6
|
+
class RoutePolicy
|
|
7
|
+
def advertisement(node, segment)
|
|
8
|
+
family = node.bgp.fetch(:vrfs, {}).dig(segment.vrf, :address_families, "ipv4 unicast")
|
|
9
|
+
return :missing unless family && family[:advertise_evpn]
|
|
10
|
+
|
|
11
|
+
rules = family.fetch(:redistribute, []).select { |rule| %w[direct connected].include?(rule[:protocol]) }
|
|
12
|
+
return :missing if rules.empty?
|
|
13
|
+
|
|
14
|
+
decisions = rules.map { |rule| rule[:route_map] ? route_map(node, rule[:route_map], segment.prefix) : :permitted }
|
|
15
|
+
return :permitted if decisions.include?(:permitted)
|
|
16
|
+
|
|
17
|
+
decisions.include?(:unknown) ? :unknown : :denied
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def route_map(node, name, prefix)
|
|
21
|
+
entries = node.route_maps[name]
|
|
22
|
+
return :unknown unless entries
|
|
23
|
+
|
|
24
|
+
entries.sort_by { |entry| entry[:sequence] }.each do |entry|
|
|
25
|
+
return :unknown if entry[:additional_matches].any?
|
|
26
|
+
|
|
27
|
+
decisions = entry[:prefix_lists].map { |list| prefix_list(node, list, prefix) }
|
|
28
|
+
return :unknown if decisions.include?(:unknown)
|
|
29
|
+
next unless decisions.empty? || decisions.include?(:permitted)
|
|
30
|
+
|
|
31
|
+
return entry[:action] == :permit ? :permitted : :denied
|
|
32
|
+
end
|
|
33
|
+
:denied
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def prefix_list(node, name, prefix)
|
|
37
|
+
entries = node.prefix_lists[name]
|
|
38
|
+
return :unknown unless entries
|
|
39
|
+
|
|
40
|
+
network = IPAddr.new(prefix)
|
|
41
|
+
length = Integer(prefix.split("/").last, 10)
|
|
42
|
+
entries.sort_by { |entry| entry[:sequence] }.each do |entry|
|
|
43
|
+
parent = IPAddr.new(entry[:prefix])
|
|
44
|
+
bits = Integer(entry[:prefix].split("/").last, 10)
|
|
45
|
+
minimum = entry[:ge] || bits
|
|
46
|
+
maximum = entry[:le] || (entry[:ge] ? 32 : bits)
|
|
47
|
+
next unless parent.include?(network) && length.between?(minimum, maximum)
|
|
48
|
+
|
|
49
|
+
return entry[:action] == :permit ? :permitted : :denied
|
|
50
|
+
end
|
|
51
|
+
:denied
|
|
52
|
+
rescue ArgumentError
|
|
53
|
+
:unknown
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Vxlan
|
|
5
|
+
class RuntimeResult
|
|
6
|
+
STATUSES = %i[complete partial unsupported failed not_requested].freeze
|
|
7
|
+
attr_reader :dimension, :status, :records, :issues
|
|
8
|
+
|
|
9
|
+
def initialize(dimension:, status:, records: [], issues: [])
|
|
10
|
+
Validation.invalid!("invalid runtime result status") unless STATUSES.include?(status)
|
|
11
|
+
Validation.invalid!("dimension must be a Symbol or String") unless dimension.is_a?(Symbol) || dimension.is_a?(String)
|
|
12
|
+
Validation.invalid!("runtime records and issues must be arrays") unless records.is_a?(Array) && issues.is_a?(Array)
|
|
13
|
+
@dimension = dimension.to_sym
|
|
14
|
+
@status = status
|
|
15
|
+
@records = Values.copy(records)
|
|
16
|
+
@issues = Values.copy(issues)
|
|
17
|
+
freeze
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def complete? = status == :complete
|
|
21
|
+
def to_h = { dimension: dimension, status: status, records: records, issues: issues }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Vxlan
|
|
5
|
+
# A routed L2 segment; IDs and gateway are explicit, never guessed from a name.
|
|
6
|
+
class Segment
|
|
7
|
+
attr_reader :vlan_id, :vni, :vrf, :prefix, :gateway, :anycast, :acl_bindings
|
|
8
|
+
|
|
9
|
+
def initialize(vlan_id:, vni:, vrf:, prefix:, gateway:, anycast: true, acl_bindings: {})
|
|
10
|
+
@vlan_id = Validation.integer(vlan_id, "vlan_id", 1..4094)
|
|
11
|
+
@vni = Validation.integer(vni, "vni", 1..16_777_215)
|
|
12
|
+
@vrf = Validation.name(vrf, "vrf")
|
|
13
|
+
@prefix = Validation.prefix(prefix)
|
|
14
|
+
@gateway = Validation.address(gateway, "gateway")
|
|
15
|
+
network = IPAddr.new(@prefix)
|
|
16
|
+
ip = IPAddr.new(@gateway)
|
|
17
|
+
Validation.invalid!("gateway must be inside prefix") unless network.ipv4? && ip.ipv4? && network.include?(ip)
|
|
18
|
+
Validation.invalid!("gateway cannot be the network or broadcast address") if prefix_length < 31 && [network.to_range.first,
|
|
19
|
+
network.to_range.last].include?(ip)
|
|
20
|
+
Validation.invalid!("anycast must be true or false") unless [true, false].include?(anycast)
|
|
21
|
+
Validation.invalid!("acl_bindings must map in/out to ACL names") unless acl_bindings.is_a?(Hash) && (acl_bindings.keys - %i[in out]).empty?
|
|
22
|
+
acl_bindings.each_value { |value| Validation.name(value, "ACL name") }
|
|
23
|
+
@anycast = anycast
|
|
24
|
+
@acl_bindings = Values.copy(acl_bindings)
|
|
25
|
+
freeze
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def prefix_length = Integer(prefix.split("/").last, 10)
|
|
29
|
+
def interface_address = "#{gateway}/#{prefix_length}"
|
|
30
|
+
def identity = [vrf, prefix].freeze
|
|
31
|
+
def to_h = { vlan_id: vlan_id, vni: vni, vrf: vrf, prefix: prefix, gateway: gateway, anycast: anycast, acl_bindings: acl_bindings }
|
|
32
|
+
def ==(other) = other.is_a?(Segment) && to_h == other.to_h
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Vxlan
|
|
5
|
+
class SegmentPlan
|
|
6
|
+
attr_reader :segment, :plans, :status, :fabric_key, :baseline_digest, :target_count, :complete_count
|
|
7
|
+
|
|
8
|
+
def initialize(segment:, plans:, status:, fabric_key:, baseline_digest:, target_count:, complete_count:)
|
|
9
|
+
@segment = segment
|
|
10
|
+
@plans = plans.dup.freeze
|
|
11
|
+
@status = status
|
|
12
|
+
@fabric_key = fabric_key.dup.freeze
|
|
13
|
+
@baseline_digest = baseline_digest.dup.freeze
|
|
14
|
+
@target_count = target_count
|
|
15
|
+
@complete_count = complete_count
|
|
16
|
+
freeze
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_h
|
|
20
|
+
{ segment: segment.to_h, plans: plans.map(&:payload), status: status, fabric_key: fabric_key,
|
|
21
|
+
baseline_digest: baseline_digest, target_count: target_count, complete_count: complete_count }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Vxlan
|
|
5
|
+
# 基于传入的 Fabric 进行纯增量规划;冲突或不完整的基线
|
|
6
|
+
# 会阻止命令生成;已经存在的组件不重复改写。
|
|
7
|
+
class SegmentPlanner
|
|
8
|
+
def plan(fabric:, segment:, target_keys: nil, replication: :bgp, multicast_group: nil, prefix_list: nil)
|
|
9
|
+
Validation.invalid!("fabric and segment must be VXLAN domain objects") unless fabric.is_a?(Fabric) && segment.is_a?(Segment)
|
|
10
|
+
Validation.invalid!("replication must be bgp or multicast") unless %i[bgp multicast].include?(replication)
|
|
11
|
+
if replication == :multicast
|
|
12
|
+
multicast_group = Validation.address(multicast_group, "multicast_group")
|
|
13
|
+
Validation.invalid!("multicast group must be in 224.0.0.0/4") unless IPAddr.new("224.0.0.0/4").include?(IPAddr.new(multicast_group))
|
|
14
|
+
end
|
|
15
|
+
Validation.name(prefix_list, "prefix-list") if prefix_list
|
|
16
|
+
nodes = selected_nodes(fabric, target_keys)
|
|
17
|
+
validate_conflicts!(fabric, segment)
|
|
18
|
+
plans = []
|
|
19
|
+
complete_count = 0
|
|
20
|
+
observed_count = 0
|
|
21
|
+
nodes.each do |node|
|
|
22
|
+
validate_node!(node, segment)
|
|
23
|
+
components = missing_components(node, segment, replication, multicast_group, prefix_list)
|
|
24
|
+
observed_count += 1 if node.vlan_vnis.key?(segment.vlan_id) || node.nve.fetch(:members, {}).key?(segment.vni)
|
|
25
|
+
if components.empty?
|
|
26
|
+
complete_count += 1
|
|
27
|
+
next
|
|
28
|
+
end
|
|
29
|
+
sequence = prefix_list && next_sequence(node, prefix_list)
|
|
30
|
+
commands = CiscoNxos::SegmentRenderer.new.render(segment: segment, components: components, nve_interface: node.nve.fetch(:interface),
|
|
31
|
+
replication: replication, multicast_group: multicast_group,
|
|
32
|
+
prefix_list: prefix_list, prefix_sequence: sequence)
|
|
33
|
+
target = Target.new(key: node.key, host: node.management_ip, platform: :cisco_nxos, attributes: { role: node.role, fabric_key: fabric.key })
|
|
34
|
+
expected = { segment: segment.to_h, replication: replication, multicast_group: multicast_group, prefix_list: prefix_list }
|
|
35
|
+
step = Deployment::Step.new(id: "segment-#{segment.vlan_id}-#{segment.vni}", action: "vxlan_segment",
|
|
36
|
+
parameters: { commands: commands, components: components }, expected: expected)
|
|
37
|
+
plans << Deployment::Plan.new(target: target, steps: [step], preconditions: { config_digest: node.config_digest, hostname: node.hostname },
|
|
38
|
+
metadata: { domain: "vxlan", fabric_key: fabric.key, baseline_digest: fabric.digest,
|
|
39
|
+
prefix: segment.prefix, vrf: segment.vrf })
|
|
40
|
+
end
|
|
41
|
+
SegmentPlan.new(segment: segment, plans: plans, status: if plans.empty?
|
|
42
|
+
:existing
|
|
43
|
+
else
|
|
44
|
+
observed_count.positive? ? :partial : :new
|
|
45
|
+
end,
|
|
46
|
+
fabric_key: fabric.key, baseline_digest: fabric.digest, target_count: nodes.size, complete_count: complete_count)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def selected_nodes(fabric, keys)
|
|
52
|
+
Validation.invalid!("fabric evidence must be complete before planning", code: :incomplete_snapshot) unless fabric.complete?
|
|
53
|
+
keys ||= fabric.vteps.map(&:key)
|
|
54
|
+
Validation.invalid!("target_keys must be unique nonempty node keys") unless keys.is_a?(Array) && !keys.empty? && keys.uniq.size == keys.size
|
|
55
|
+
nodes = keys.map { |key| fabric.node(key) }
|
|
56
|
+
Validation.invalid!("all selected targets must be VTEPs") unless nodes.all? { |node| node&.vtep? }
|
|
57
|
+
nodes
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def validate_conflicts!(fabric, segment)
|
|
61
|
+
network = IPAddr.new(segment.prefix)
|
|
62
|
+
fabric.nodes.each do |node|
|
|
63
|
+
existing_vni = node.vlan_vnis[segment.vlan_id]
|
|
64
|
+
conflict!(node, :vlan_conflict) if existing_vni && existing_vni != segment.vni
|
|
65
|
+
conflict!(node, :vni_conflict) if node.vlan_vnis.any? { |vlan, vni| vni == segment.vni && vlan != segment.vlan_id }
|
|
66
|
+
conflict!(node, :vni_conflict) if node.vrfs.values.any? { |vrf| vrf[:vni] == segment.vni }
|
|
67
|
+
node.segments.each do |existing|
|
|
68
|
+
same_address = existing.vrf == segment.vrf && existing.prefix == segment.prefix && existing.gateway == segment.gateway
|
|
69
|
+
conflict!(node, :segment_conflict) if existing.vlan_id == segment.vlan_id && !same_address
|
|
70
|
+
next unless existing.vrf == segment.vrf
|
|
71
|
+
|
|
72
|
+
other = IPAddr.new(existing.prefix)
|
|
73
|
+
conflict!(node, :prefix_conflict) if (network.include?(other) || other.include?(network)) && existing.vlan_id != segment.vlan_id
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def validate_node!(node, segment)
|
|
79
|
+
Validation.invalid!("management IP is required", code: :management_ip_missing, details: { node_key: node.key }) unless node.management_ip
|
|
80
|
+
conflict!(node, :nve_missing) unless node.nve[:interface] && node.nve[:source_interface] && node.nve[:host_reachability] == "bgp"
|
|
81
|
+
vrf = node.vrfs[segment.vrf]
|
|
82
|
+
conflict!(node, :vrf_prerequisite_missing) unless vrf && vrf[:vni] && node.nve.fetch(:members, {}).dig(vrf[:vni], :associate_vrf)
|
|
83
|
+
conflict!(node, :anycast_mac_missing) if segment.anycast && !node.anycast_mac
|
|
84
|
+
segment.acl_bindings.each_value { |name| conflict!(node, :acl_missing) unless node.acls.key?(name) }
|
|
85
|
+
existing = node.interfaces["Vlan#{segment.vlan_id}"] || node.interfaces["vlan#{segment.vlan_id}"]
|
|
86
|
+
return unless existing
|
|
87
|
+
|
|
88
|
+
conflict!(node, :svi_vrf_conflict) if existing[:vrf] && existing[:vrf] != segment.vrf
|
|
89
|
+
conflict!(node, :svi_address_conflict) if existing[:addresses].any? && existing[:addresses] != [segment.interface_address]
|
|
90
|
+
existing[:acl_bindings].each do |direction, name|
|
|
91
|
+
conflict!(node, :acl_conflict) if segment.acl_bindings[direction] && segment.acl_bindings[direction] != name
|
|
92
|
+
end
|
|
93
|
+
conflict!(node, :anycast_conflict) if existing[:anycast] && !segment.anycast
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def missing_components(node, segment, replication, multicast_group, prefix_list)
|
|
97
|
+
missing = []
|
|
98
|
+
missing << :vlan unless node.vlan_vnis[segment.vlan_id] == segment.vni
|
|
99
|
+
interface = node.interfaces["Vlan#{segment.vlan_id}"] || {}
|
|
100
|
+
missing << :svi_vrf unless interface[:vrf] == segment.vrf
|
|
101
|
+
missing << :svi_address unless interface[:addresses] == [segment.interface_address]
|
|
102
|
+
missing << :svi_enable if interface.empty? || interface[:shutdown] == true
|
|
103
|
+
missing << :svi_anycast if segment.anycast && !interface[:anycast]
|
|
104
|
+
segment.acl_bindings.each { |direction, name| missing << :"svi_acl_#{direction}" unless interface.fetch(:acl_bindings, {})[direction] == name }
|
|
105
|
+
member = node.nve.fetch(:members, {})[segment.vni]
|
|
106
|
+
if member
|
|
107
|
+
incompatible = member[:associate_vrf] || (member[:replication] && member[:replication] != replication)
|
|
108
|
+
incompatible ||= member[:multicast_group] && member[:multicast_group] != multicast_group
|
|
109
|
+
conflict!(node, :nve_member_conflict) if incompatible
|
|
110
|
+
end
|
|
111
|
+
missing << :nve unless member && (replication == :bgp ? member[:replication] == :bgp : member[:multicast_group] == multicast_group)
|
|
112
|
+
evpn = node.evpn[segment.vni]
|
|
113
|
+
if evpn
|
|
114
|
+
conflict!(node, :evpn_type_conflict) unless evpn[:type] == :l2
|
|
115
|
+
targets = evpn[:route_targets]
|
|
116
|
+
directions = targets.flat_map { |target| target[:direction] == :both ? %i[import export] : [target[:direction]] }
|
|
117
|
+
unless evpn[:rd] && (%i[import export] - directions).empty?
|
|
118
|
+
conflict!(node, :evpn_policy_conflict) if targets.any? { |target| target[:target] != "auto" } || (evpn[:rd] && evpn[:rd] != "auto")
|
|
119
|
+
missing << :evpn
|
|
120
|
+
end
|
|
121
|
+
else
|
|
122
|
+
missing << :evpn
|
|
123
|
+
end
|
|
124
|
+
if prefix_list && RoutePolicy.new.advertisement(node, segment) != :permitted
|
|
125
|
+
validate_advertisement!(node, segment, prefix_list)
|
|
126
|
+
missing << :advertisement
|
|
127
|
+
end
|
|
128
|
+
missing
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def validate_advertisement!(node, segment, prefix_list)
|
|
132
|
+
family = node.bgp.fetch(:vrfs, {}).dig(segment.vrf, :address_families, "ipv4 unicast")
|
|
133
|
+
conflict!(node, :advertisement_prerequisite_missing) unless family && family[:advertise_evpn]
|
|
134
|
+
routes = family[:redistribute].select { |rule| %w[direct connected].include?(rule[:protocol]) && rule[:route_map] }
|
|
135
|
+
matching = routes.any? do |rule|
|
|
136
|
+
entries = node.route_maps[rule[:route_map]]
|
|
137
|
+
entries && entries.size == 1 && entries.first[:action] == :permit &&
|
|
138
|
+
entries.first[:additional_matches].empty? && entries.first[:prefix_lists] == [prefix_list]
|
|
139
|
+
end
|
|
140
|
+
conflict!(node, :advertisement_policy_unsupported) unless matching && node.prefix_lists.key?(prefix_list)
|
|
141
|
+
# An earlier deny would shadow an appended permit; do not claim remediation.
|
|
142
|
+
trial = node.prefix_lists[prefix_list]
|
|
143
|
+
conflict!(node, :advertisement_policy_conflict) if trial.any? { |entry| entry[:action] == :deny }
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def next_sequence(node, prefix_list)
|
|
147
|
+
value = node.prefix_lists.fetch(prefix_list, []).map { |entry| entry[:sequence] }.max.to_i + 5
|
|
148
|
+
Validation.integer(value, "prefix-list sequence", 1..4_294_967_294)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def conflict!(node, code)
|
|
152
|
+
Validation.invalid!("VXLAN planning prerequisite or conflict: #{code}", code: code, details: { node_key: node.key })
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Netstack
|
|
6
|
+
module Vxlan
|
|
7
|
+
class Snapshot
|
|
8
|
+
attr_reader :fabric, :runtime, :collection, :observed_at
|
|
9
|
+
|
|
10
|
+
def initialize(fabric:, runtime: {}, collection: {}, observed_at: Time.now.utc)
|
|
11
|
+
Validation.invalid!("fabric must be a Fabric") unless fabric.is_a?(Fabric)
|
|
12
|
+
Validation.invalid!("runtime and collection must be hashes") unless runtime.is_a?(Hash) && collection.is_a?(Hash)
|
|
13
|
+
@fabric = fabric
|
|
14
|
+
@collection = Values.copy(collection)
|
|
15
|
+
@runtime = runtime.to_h do |key, dimensions|
|
|
16
|
+
Validation.invalid!("runtime dimensions must contain RuntimeResult values") unless dimensions.is_a?(Hash) && dimensions.values.all?(RuntimeResult)
|
|
17
|
+
[key.dup.freeze, dimensions.dup.freeze]
|
|
18
|
+
end.freeze
|
|
19
|
+
@observed_at = observed_at.is_a?(Time) ? observed_at.getutc.iso8601(6).freeze : Time.iso8601(observed_at).getutc.iso8601(6).freeze
|
|
20
|
+
freeze
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def complete?
|
|
24
|
+
fabric.complete? && collection.values.all? { |value| value == :complete } && runtime.values.all? { |dimensions| dimensions.values.all?(&:complete?) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def to_h
|
|
28
|
+
{ fabric: fabric.to_h, runtime: runtime.transform_values do |dimensions|
|
|
29
|
+
dimensions.transform_values(&:to_h)
|
|
30
|
+
end, collection: collection, observed_at: observed_at }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ipaddr"
|
|
4
|
+
require "digest"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module Netstack
|
|
8
|
+
module Vxlan
|
|
9
|
+
# Validation and canonical fingerprints shared by the VXLAN value objects.
|
|
10
|
+
module Validation
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def integer(value, name, range)
|
|
14
|
+
invalid!("#{name} must be an Integer in #{range}") unless value.is_a?(Integer) && range.cover?(value)
|
|
15
|
+
value
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def name(value, field)
|
|
19
|
+
invalid!("#{field} must be a nonempty CLI identifier") unless value.is_a?(String) && /\A[a-zA-Z0-9_.:-]+\z/.match?(value)
|
|
20
|
+
value.dup.freeze
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def address(value, field = "address")
|
|
24
|
+
invalid!("#{field} must be an IP address") unless value.is_a?(String) && !value.include?("/")
|
|
25
|
+
IPAddr.new(value).to_s.freeze
|
|
26
|
+
rescue IPAddr::InvalidAddressError, IPAddr::AddressFamilyError
|
|
27
|
+
invalid!("#{field} must be an IP address")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def prefix(value)
|
|
31
|
+
invalid!("prefix must be an IPv4 network in CIDR notation") unless value.is_a?(String) && %r{\A\d+(?:\.\d+){3}/\d+\z}.match?(value)
|
|
32
|
+
network = IPAddr.new(value)
|
|
33
|
+
bits = Integer(value.split("/").last, 10)
|
|
34
|
+
invalid!("prefix must specify a network address") unless network.ipv4? && bits.between?(0, 32) && network.to_s == value.split("/").first
|
|
35
|
+
"#{network}/#{bits}".freeze
|
|
36
|
+
rescue IPAddr::InvalidAddressError
|
|
37
|
+
invalid!("prefix must be an IPv4 network in CIDR notation")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def fingerprint(value)
|
|
41
|
+
Digest::SHA256.hexdigest(JSON.generate(canonical(value)))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def canonical(value)
|
|
45
|
+
case value
|
|
46
|
+
when Hash then value.keys.sort_by(&:to_s).to_h { |key| [key.to_s, canonical(value.fetch(key))] }
|
|
47
|
+
when Array then value.map { |item| canonical(item) }
|
|
48
|
+
when Symbol then value.to_s
|
|
49
|
+
else value.respond_to?(:to_h) ? canonical(value.to_h) : value
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def invalid!(message, code: :invalid_vxlan_input, details: {})
|
|
54
|
+
raise InputError.new(message, code: code, stage: :validation, details: details)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Vxlan
|
|
5
|
+
# 根据解析后的回读确认配置意图;不证明实际流量转发成功。
|
|
6
|
+
# 证据不完整或不匹配时保留未知状态,不能据此授权重放。
|
|
7
|
+
class Verifier
|
|
8
|
+
def verify(node:, expected:)
|
|
9
|
+
return { status: :unknown, evidence: { reason: :configuration_incomplete } } unless node.complete?
|
|
10
|
+
|
|
11
|
+
expected = symbolize(expected)
|
|
12
|
+
segment = Segment.new(**expected.fetch(:segment))
|
|
13
|
+
actual = node.segment(segment.vlan_id)
|
|
14
|
+
missing = []
|
|
15
|
+
missing << :vlan unless node.vlan_vnis[segment.vlan_id] == segment.vni
|
|
16
|
+
unless actual && actual.vrf == segment.vrf && actual.prefix == segment.prefix && actual.gateway == segment.gateway && actual.anycast == segment.anycast
|
|
17
|
+
missing << :svi
|
|
18
|
+
end
|
|
19
|
+
# SVI 标志和引用不能证明依赖仍存在;回读须保持规划时要求的前提。
|
|
20
|
+
missing << :anycast_mac if segment.anycast && !node.anycast_mac
|
|
21
|
+
missing << :acl unless actual && segment.acl_bindings.all? { |direction, name| actual.acl_bindings[direction] == name && node.acls.key?(name) }
|
|
22
|
+
missing << :svi_shutdown if node.interfaces.dig("Vlan#{segment.vlan_id}", :shutdown) == true
|
|
23
|
+
member = node.nve.fetch(:members, {})[segment.vni]
|
|
24
|
+
replication_ok = if expected[:replication].to_sym == :bgp
|
|
25
|
+
member && member[:replication] == :bgp
|
|
26
|
+
else
|
|
27
|
+
member && member[:multicast_group] == expected[:multicast_group]
|
|
28
|
+
end
|
|
29
|
+
missing << :nve unless member && !member[:associate_vrf] && replication_ok
|
|
30
|
+
evpn = node.evpn[segment.vni]
|
|
31
|
+
directions = if evpn
|
|
32
|
+
evpn[:route_targets].flat_map { |target| target[:direction] == :both ? %i[import export] : [target[:direction]] }
|
|
33
|
+
else
|
|
34
|
+
[]
|
|
35
|
+
end
|
|
36
|
+
missing << :evpn unless evpn && evpn[:type] == :l2 && evpn[:rd] && (%i[import export] - directions).empty?
|
|
37
|
+
missing << :advertisement if expected[:prefix_list] && RoutePolicy.new.advertisement(node, segment) != :permitted
|
|
38
|
+
{ status: missing.empty? ? :verified : :unknown, evidence: { config_digest: node.config_digest, missing: missing } }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def symbolize(value)
|
|
44
|
+
case value
|
|
45
|
+
when Hash then value.to_h { |key, entry| [key.to_sym, symbolize(entry)] }
|
|
46
|
+
when Array then value.map { |entry| symbolize(entry) }
|
|
47
|
+
else value
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Vxlan
|
|
5
|
+
class VpcDomain
|
|
6
|
+
attr_reader :id, :peer_keepalive, :peer_links, :member_ports, :peer_gateway
|
|
7
|
+
|
|
8
|
+
def initialize(id:, peer_keepalive: {}, peer_links: [], member_ports: {}, peer_gateway: false)
|
|
9
|
+
@id = Validation.integer(id, "vPC domain", 1..1000)
|
|
10
|
+
@peer_keepalive = Values.copy(peer_keepalive)
|
|
11
|
+
@peer_links = Values.copy(peer_links)
|
|
12
|
+
@member_ports = Values.copy(member_ports)
|
|
13
|
+
@peer_gateway = peer_gateway
|
|
14
|
+
freeze
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_h = { id: id, peer_keepalive: peer_keepalive, peer_links: peer_links, member_ports: member_ports, peer_gateway: peer_gateway }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|