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,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Netstack
|
|
7
|
+
module Configuration
|
|
8
|
+
# Content is deliberately absent from inspect, summary and deployment plans.
|
|
9
|
+
class Artifact
|
|
10
|
+
PLATFORMS = Values.copy({ "h3c" => %w[h3c], "cisco" => %w[cisco_nxos], "hillstone" => %w[hillstone],
|
|
11
|
+
"paloalto" => %w[palo_alto], "radware" => %w[radware] })
|
|
12
|
+
attr_reader :filename, :scenario, :vendor, :created_at, :sha256, :template_sha256, :sections
|
|
13
|
+
|
|
14
|
+
def self.script(target:, content:, filename: "configuration.txt", created_at: Time.now.utc)
|
|
15
|
+
raise InputError, "manual script target must be a Target" unless target.is_a?(Target)
|
|
16
|
+
|
|
17
|
+
new(filename: filename, content: content, scenario: "manual", vendor: target.platform, created_at: created_at,
|
|
18
|
+
template_sha256: nil, target: target)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def initialize(filename:, content:, scenario:, vendor:, created_at:, template_sha256:, sections: {}, target: nil)
|
|
22
|
+
@filename = Values.text(filename, name: "artifact filename")
|
|
23
|
+
raise InputError, "artifact filename must be a plain filename" unless filename.match?(/\A[a-zA-Z0-9_.-]+\.txt\z/)
|
|
24
|
+
unless content.is_a?(String) && content.valid_encoding? && !content.empty? && !content.match?(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/)
|
|
25
|
+
raise InputError, "artifact must contain valid configuration text"
|
|
26
|
+
end
|
|
27
|
+
@content = content.dup.freeze
|
|
28
|
+
@scenario = Values.text(scenario, name: "scenario")
|
|
29
|
+
@vendor = Values.text(vendor, name: "vendor")
|
|
30
|
+
@created_at = created_at.getutc.freeze
|
|
31
|
+
@sha256 = Digest::SHA256.hexdigest(content).freeze
|
|
32
|
+
@template_sha256 = Values.text(template_sha256, name: "template digest", optional: true)
|
|
33
|
+
if scenario == "manual"
|
|
34
|
+
raise InputError, "manual scripts require an explicit target" unless target.is_a?(Target) && target.platform == vendor
|
|
35
|
+
@target = target
|
|
36
|
+
elsif target
|
|
37
|
+
raise InputError, "generated templates cannot override their platform contract"
|
|
38
|
+
end
|
|
39
|
+
unless sections.is_a?(Hash) && sections.values.all? { |section| section.is_a?(Artifact) && section.executable? }
|
|
40
|
+
raise InputError, "artifact sections must be single-target artifacts"
|
|
41
|
+
end
|
|
42
|
+
@sections = sections.to_h { |key, value| [Values.text(key, name: "section key"), value] }.freeze
|
|
43
|
+
freeze
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def content = @content
|
|
47
|
+
def executable? = sections.empty?
|
|
48
|
+
def section(name) = sections.fetch(name.to_s)
|
|
49
|
+
|
|
50
|
+
def summary
|
|
51
|
+
{ filename: filename, scenario: scenario, vendor: vendor, created_at: created_at.iso8601,
|
|
52
|
+
sha256: sha256, template_sha256: template_sha256, bytes: content.bytesize,
|
|
53
|
+
executable: executable?, sections: sections.keys }.freeze
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def inspect = "#<#{self.class.name} filename=#{filename.inspect} sha256=#{sha256} bytes=#{content.bytesize}>"
|
|
57
|
+
|
|
58
|
+
def plan(target:, artifact_ref:, expected:, request_no: nil, target_id: nil)
|
|
59
|
+
raise InputError, "select a single-target artifact section before planning" unless executable?
|
|
60
|
+
raise InputError, "configuration verification expectations are required" unless expected.is_a?(Hash) && !expected.empty?
|
|
61
|
+
validate_target!(target)
|
|
62
|
+
|
|
63
|
+
parameters = { artifact_ref: Values.text(artifact_ref, name: "artifact reference"), sha256: sha256 }
|
|
64
|
+
parameters[:request_no] = Values.text(request_no, name: "request number") unless request_no.nil?
|
|
65
|
+
unless target_id.nil?
|
|
66
|
+
raise InputError, "target id must be text or an integer" unless target_id.is_a?(String) || target_id.is_a?(Integer)
|
|
67
|
+
parameters[:target_id] = Values.text(target_id.to_s, name: "target id")
|
|
68
|
+
end
|
|
69
|
+
Deployment::Plan.new(target: target, steps: [Deployment::Step.new(id: "configuration", action: "configuration",
|
|
70
|
+
parameters: parameters, expected: expected)], metadata: { scenario: scenario, vendor: vendor, template_sha256: template_sha256 })
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def validate_target!(target)
|
|
74
|
+
if @target
|
|
75
|
+
return if target.is_a?(Target) && target == @target
|
|
76
|
+
|
|
77
|
+
raise InputError, "manual script belongs to a different target"
|
|
78
|
+
end
|
|
79
|
+
return if target.is_a?(Target) && PLATFORMS.fetch(vendor, []).include?(target.platform)
|
|
80
|
+
raise InputError, "artifact vendor and target platform do not match"
|
|
81
|
+
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Configuration
|
|
5
|
+
# Re-resolve and verify immediately before submission; do not cache mutable host records.
|
|
6
|
+
class ArtifactResolver
|
|
7
|
+
def initialize(resolver:)
|
|
8
|
+
raise InputError, "artifact resolver must be callable" unless resolver.respond_to?(:call)
|
|
9
|
+
|
|
10
|
+
@resolver = resolver
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def resolve(step)
|
|
14
|
+
raise InputError, "unsupported configuration action" unless step.action == "configuration"
|
|
15
|
+
|
|
16
|
+
reference = step.parameters.fetch(:artifact_ref)
|
|
17
|
+
digest = step.parameters.fetch(:sha256)
|
|
18
|
+
raise InputError, "invalid artifact reference or digest" unless reference.is_a?(String) && !reference.empty? && digest.is_a?(String) && digest.match?(/\A[0-9a-f]{64}\z/)
|
|
19
|
+
|
|
20
|
+
artifact = @resolver.call(reference)
|
|
21
|
+
unless artifact.is_a?(Artifact) && artifact.executable? && artifact.sha256 == digest
|
|
22
|
+
raise Error.new("configuration artifact is unavailable or changed", code: :artifact_changed, stage: :preflight)
|
|
23
|
+
end
|
|
24
|
+
artifact
|
|
25
|
+
rescue KeyError
|
|
26
|
+
raise InputError, "configuration plan requires artifact_ref and sha256"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Adapted from Motor Netconf scenario definitions and safe template dialect.
|
|
4
|
+
require_relative "../support"
|
|
5
|
+
|
|
6
|
+
module Netstack
|
|
7
|
+
module Configuration
|
|
8
|
+
module Bootstrap
|
|
9
|
+
module Payload
|
|
10
|
+
H3C_STP_PRIORITIES = {
|
|
11
|
+
"core" => 4096, "aggregation" => 8192, "access" => 32_768
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def bootstrap(data)
|
|
17
|
+
data["access_auth_enabled"] = false unless data.key?("access_auth_enabled")
|
|
18
|
+
stp_priority = H3C_STP_PRIORITIES[data.fetch("device_role")]
|
|
19
|
+
data["stp_priority"] = stp_priority if stp_priority
|
|
20
|
+
cidr = data.fetch("mgmt")
|
|
21
|
+
range = IPAddr.new(cidr).to_range
|
|
22
|
+
data.merge!(
|
|
23
|
+
"mgmt_ip" => cidr.split("/").first,
|
|
24
|
+
"mgmt_mask" => mask(cidr),
|
|
25
|
+
"mgmt_gateway" => data["mgmt_gateway"].presence || ipv4(range.last.to_i - 1),
|
|
26
|
+
"loopback_ip" => data["loopback"]&.split("/")&.first
|
|
27
|
+
)
|
|
28
|
+
if data.key?("underlay_enabled")
|
|
29
|
+
decorate_cisco_fabric!(data)
|
|
30
|
+
else
|
|
31
|
+
data["h3c_uplinks"] = data.fetch("uplink_interfaces").map.with_index do |interface, index|
|
|
32
|
+
{ "interface" => interface, "description" => "UPLINK_#{index + 1}" }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
decorate_bootstrap_system!(data.fetch("system"))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def decorate_cisco_fabric!(data)
|
|
39
|
+
overlay = data["overlay_enabled"]
|
|
40
|
+
role = data.fetch("device_role")
|
|
41
|
+
data["nve_enabled"] = overlay && %w[leaf border].include?(role)
|
|
42
|
+
data["route_reflector"] = overlay && role == "spine"
|
|
43
|
+
data["bgp_neighbors"] = Array(data["overlay_neighbors"]).map { |address| { "address" => address } }
|
|
44
|
+
data["external_bgp_neighbors"] ||= []
|
|
45
|
+
data["vtep_secondary_enabled"] = false
|
|
46
|
+
data["advertise_pip"] = false
|
|
47
|
+
return unless data["nve_enabled"]
|
|
48
|
+
|
|
49
|
+
data["vtep_secondary_enabled"] = data["vtep_secondary"].present?
|
|
50
|
+
data["advertise_pip"] = data["vpc_enabled"] && data["vtep_secondary_enabled"]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def decorate_bootstrap_system!(system)
|
|
54
|
+
system["tacacs_entries"] = entries(system.fetch("tacacs_servers"))
|
|
55
|
+
system["radius_entries"] = entries(Array(system["radius_servers"]))
|
|
56
|
+
system["ssh_acl_entries"] = system.fetch("ssh_acl_cidrs").map.with_index do |cidr, index|
|
|
57
|
+
{ "sequence" => (index + 1) * 10, "wildcard" => wildcard(cidr) }
|
|
58
|
+
end
|
|
59
|
+
system["local_users"].each do |user|
|
|
60
|
+
user["h3c_role"] = user.fetch("role") == "admin" ? "network-admin" : "network-operator"
|
|
61
|
+
user["cisco_privilege"] = { "admin" => 15, "operator" => 5 }.fetch(user.fetch("role"), 1)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Adapted from Motor Netconf scenario definitions and safe template dialect.
|
|
4
|
+
require_relative "../support"
|
|
5
|
+
|
|
6
|
+
module Netstack
|
|
7
|
+
module Configuration
|
|
8
|
+
module Bootstrap
|
|
9
|
+
module Validator
|
|
10
|
+
H3C_ROLES = %w[access aggregation core].freeze
|
|
11
|
+
CISCO_ROLES = %w[leaf spine border].freeze
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def bootstrap_roles
|
|
16
|
+
@vendor == "cisco" ? CISCO_ROLES : H3C_ROLES
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def validate_bootstrap_gateway!(result, value)
|
|
20
|
+
if value.nil? || value == ""
|
|
21
|
+
if result.fetch("mgmt").split("/").first == last_usable(result.fetch("mgmt"))
|
|
22
|
+
raise self.class::Error, "mgmt_gateway is required when management address is the last usable address"
|
|
23
|
+
end
|
|
24
|
+
return
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
gateway = ipv4!(value, "mgmt_gateway")
|
|
28
|
+
network = IPAddr.new(result.fetch("mgmt"))
|
|
29
|
+
range = network.to_range
|
|
30
|
+
unless network.include?(gateway) && ![ range.first.to_s, range.last.to_s ].include?(gateway)
|
|
31
|
+
raise self.class::Error, "mgmt_gateway must be a usable host in the management subnet"
|
|
32
|
+
end
|
|
33
|
+
if gateway == result.fetch("mgmt").split("/").first
|
|
34
|
+
raise self.class::Error, "mgmt_gateway must differ from the management address"
|
|
35
|
+
end
|
|
36
|
+
result["mgmt_gateway"] = gateway
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def validate_h3c_bootstrap!(result, common, access_auth_enabled)
|
|
40
|
+
topology = take!(%w[access_interface_start access_interface_end uplink_interfaces])
|
|
41
|
+
uplinks = bootstrap_interface_list!(
|
|
42
|
+
default_if_omitted(topology["uplink_interfaces"], [ "Ten-GigabitEthernet1/0/49", "Ten-GigabitEthernet1/0/50" ]),
|
|
43
|
+
"uplink_interfaces", max: 8
|
|
44
|
+
)
|
|
45
|
+
result.merge!(
|
|
46
|
+
"access_interface_start" => interface!(default_if_omitted(topology["access_interface_start"], "GigabitEthernet1/0/1"), "access_interface_start"),
|
|
47
|
+
"access_interface_end" => interface!(default_if_omitted(topology["access_interface_end"], "GigabitEthernet1/0/48"), "access_interface_end"),
|
|
48
|
+
"uplink_interfaces" => uplinks
|
|
49
|
+
)
|
|
50
|
+
access_range = h3c_port_range!(result["access_interface_start"], result["access_interface_end"], "access interfaces")
|
|
51
|
+
reject_uplink_overlap!(access_range, uplinks)
|
|
52
|
+
if access_auth_enabled
|
|
53
|
+
result.merge!(
|
|
54
|
+
"access_auth_enabled" => true,
|
|
55
|
+
"access_auth_domain" => identifier!(common["access_auth_domain"], "access_auth_domain"),
|
|
56
|
+
"access_auth_vlan" => integer!(common["access_auth_vlan"], "access_auth_vlan", 1..4094),
|
|
57
|
+
"access_auth_interface_start" => interface!(common["access_auth_interface_start"], "access_auth_interface_start"),
|
|
58
|
+
"access_auth_interface_end" => interface!(common["access_auth_interface_end"], "access_auth_interface_end")
|
|
59
|
+
)
|
|
60
|
+
authentication_range = h3c_port_range!(result["access_auth_interface_start"], result["access_auth_interface_end"], "access authentication interfaces")
|
|
61
|
+
reject_uplink_overlap!(authentication_range, uplinks)
|
|
62
|
+
unless authentication_range.first == access_range.first && access_range.last.cover?(authentication_range.last)
|
|
63
|
+
raise self.class::Error, "access authentication range must be inside the access range"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
finish!(result)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# H3C 的 to 范围必须属于同一类型、成员和槽位,并按端口号递增。
|
|
70
|
+
def h3c_port_range!(first, last, name)
|
|
71
|
+
endpoints = [first, last].map { |value| h3c_port!(value) }
|
|
72
|
+
unless endpoints.first.first == endpoints.last.first && endpoints.first.last <= endpoints.last.last
|
|
73
|
+
raise self.class::Error, "#{name} must be ordered ports on one interface path"
|
|
74
|
+
end
|
|
75
|
+
[endpoints.first.first, endpoints.first.last..endpoints.last.last]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def h3c_port!(value)
|
|
79
|
+
match = /\A([a-z][a-z-]*[0-9]+\/[0-9]+\/)([0-9]+)\z/i.match(value)
|
|
80
|
+
raise self.class::Error, "H3C bootstrap requires physical interface paths" unless match
|
|
81
|
+
|
|
82
|
+
[match[1].downcase, Integer(match[2], 10)]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# 比较范围内所有端口,避免只比较端点而把上联口纳入接入配置。
|
|
86
|
+
def reject_uplink_overlap!(range, uplinks)
|
|
87
|
+
return unless uplinks.any? { |value| h3c_port!(value).then { |path, port| path == range.first && range.last.cover?(port) } }
|
|
88
|
+
|
|
89
|
+
raise self.class::Error, "uplink interfaces must be outside access ranges"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def validate_cisco_bootstrap!(result)
|
|
93
|
+
fabric = take!(%w[
|
|
94
|
+
underlay_enabled loopback ospf_process_id ospf_area fabric_mtu fabric_links
|
|
95
|
+
overlay_enabled local_as remote_as overlay_neighbors vtep_loopback vtep_secondary
|
|
96
|
+
external_bgp_neighbors vpc_enabled vpc_domain_id vpc_role_priority
|
|
97
|
+
vpc_peer_keepalive_destination vpc_peer_keepalive_source
|
|
98
|
+
vpc_virtual_peer_link_destination vpc_virtual_peer_link_source
|
|
99
|
+
vpc_peer_link_port_channel
|
|
100
|
+
])
|
|
101
|
+
underlay_enabled = fabric["underlay_enabled"].nil? || boolean!(fabric["underlay_enabled"], "underlay_enabled")
|
|
102
|
+
overlay_enabled = fabric["overlay_enabled"].nil? || boolean!(fabric["overlay_enabled"], "overlay_enabled")
|
|
103
|
+
vpc_enabled = fabric["vpc_enabled"].nil? ? false : boolean!(fabric["vpc_enabled"], "vpc_enabled")
|
|
104
|
+
raise self.class::Error, "overlay requires underlay" if overlay_enabled && !underlay_enabled
|
|
105
|
+
raise self.class::Error, "vPC requires overlay" if vpc_enabled && !overlay_enabled
|
|
106
|
+
raise self.class::Error, "spine role does not support vPC" if result["device_role"] == "spine" && vpc_enabled
|
|
107
|
+
|
|
108
|
+
result.merge!("underlay_enabled" => underlay_enabled, "overlay_enabled" => overlay_enabled, "vpc_enabled" => vpc_enabled)
|
|
109
|
+
validate_cisco_underlay!(result, fabric) if underlay_enabled
|
|
110
|
+
validate_cisco_overlay!(result, fabric) if overlay_enabled
|
|
111
|
+
validate_cisco_vpc!(result, fabric) if vpc_enabled
|
|
112
|
+
result["vpc_virtual_peer_link_enabled"] ||= false
|
|
113
|
+
finish!(result)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def validate_cisco_underlay!(result, fabric)
|
|
117
|
+
links = rows!(fabric["fabric_links"], "fabric_links")
|
|
118
|
+
raise self.class::Error, "fabric_links must contain 1 to 8 rows" if links.size > 8
|
|
119
|
+
links = links.map.with_index do |link, index|
|
|
120
|
+
row = exact!(link, %w[interface address description], "fabric_links[#{index}]")
|
|
121
|
+
{
|
|
122
|
+
"interface" => interface!(row["interface"], "fabric_links[#{index}].interface"),
|
|
123
|
+
"address" => cidr!(row["address"], "fabric_links[#{index}].address", host: true),
|
|
124
|
+
"description" => description!(row["description"], "fabric_links[#{index}].description")
|
|
125
|
+
}
|
|
126
|
+
end
|
|
127
|
+
interfaces = links.pluck("interface").map(&:downcase)
|
|
128
|
+
addresses = links.pluck("address").map { |value| value.split("/").first }
|
|
129
|
+
raise self.class::Error, "fabric link interfaces must be unique" unless interfaces.uniq.size == interfaces.size
|
|
130
|
+
raise self.class::Error, "fabric link addresses must be unique" unless addresses.uniq.size == addresses.size
|
|
131
|
+
|
|
132
|
+
loopback = cidr!(fabric["loopback"], "loopback", host: true)
|
|
133
|
+
raise self.class::Error, "underlay interface addresses must be unique" if addresses.include?(loopback.split("/").first)
|
|
134
|
+
result.merge!(
|
|
135
|
+
"loopback" => loopback,
|
|
136
|
+
"ospf_process_id" => identifier!(default_if_omitted(fabric["ospf_process_id"], "10"), "ospf_process_id"),
|
|
137
|
+
"ospf_area" => ospf_area!(default_if_omitted(fabric["ospf_area"], "0.0.0.0"), "ospf_area"),
|
|
138
|
+
"fabric_mtu" => integer!(default_if_omitted(fabric["fabric_mtu"], 9216), "fabric_mtu", 1500..9216),
|
|
139
|
+
"fabric_links" => links
|
|
140
|
+
)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def validate_cisco_overlay!(result, fabric)
|
|
144
|
+
neighbors = bootstrap_ipv4_list!(fabric["overlay_neighbors"], "overlay_neighbors", max: self.class::MAX_ROWS)
|
|
145
|
+
result.merge!(
|
|
146
|
+
"local_as" => asn!(fabric["local_as"], "local_as"),
|
|
147
|
+
"remote_as" => asn!(fabric["remote_as"], "remote_as"),
|
|
148
|
+
"overlay_neighbors" => neighbors
|
|
149
|
+
)
|
|
150
|
+
if result["device_role"] == "spine"
|
|
151
|
+
result["external_bgp_neighbors"] = []
|
|
152
|
+
return
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
result.merge!(
|
|
156
|
+
"vtep_loopback" => cidr!(fabric["vtep_loopback"], "vtep_loopback", host: true),
|
|
157
|
+
"vtep_secondary" => (cidr!(fabric["vtep_secondary"], "vtep_secondary", host: true) if provided?(fabric["vtep_secondary"]))
|
|
158
|
+
)
|
|
159
|
+
loopbacks = result.values_at("loopback", "vtep_loopback", "vtep_secondary").compact.map { |value| value.split("/").first }
|
|
160
|
+
raise self.class::Error, "loopback addresses must be unique" unless loopbacks.uniq.size == loopbacks.size
|
|
161
|
+
result["external_bgp_neighbors"] = result["device_role"] == "border" ? validate_external_bgp_neighbors!(fabric["external_bgp_neighbors"]) : []
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def validate_external_bgp_neighbors!(value)
|
|
165
|
+
return [] if !provided?(value) || value == []
|
|
166
|
+
rows!(value, "external_bgp_neighbors").map.with_index do |neighbor, index|
|
|
167
|
+
row = exact!(neighbor, %w[address remote_as description], "external_bgp_neighbors[#{index}]")
|
|
168
|
+
{
|
|
169
|
+
"address" => ipv4!(row["address"], "external_bgp_neighbors[#{index}].address"),
|
|
170
|
+
"remote_as" => asn!(row["remote_as"], "external_bgp_neighbors[#{index}].remote_as"),
|
|
171
|
+
"description" => optional_description(row["description"], "external_bgp_neighbors[#{index}].description")
|
|
172
|
+
}.compact
|
|
173
|
+
end.tap do |neighbors|
|
|
174
|
+
addresses = neighbors.pluck("address")
|
|
175
|
+
raise self.class::Error, "external BGP neighbors must be unique" unless addresses.uniq.size == addresses.size
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def validate_cisco_vpc!(result, fabric)
|
|
180
|
+
result.merge!(
|
|
181
|
+
"vpc_domain_id" => integer!(default_if_omitted(fabric["vpc_domain_id"], 1), "vpc_domain_id", 1..1000),
|
|
182
|
+
"vpc_role_priority" => integer!(default_if_omitted(fabric["vpc_role_priority"], 100), "vpc_role_priority", 1..65_535),
|
|
183
|
+
"vpc_peer_keepalive_destination" => ipv4!(fabric["vpc_peer_keepalive_destination"], "vpc_peer_keepalive_destination"),
|
|
184
|
+
"vpc_peer_keepalive_source" => ipv4!(fabric["vpc_peer_keepalive_source"], "vpc_peer_keepalive_source"),
|
|
185
|
+
"vpc_peer_link_port_channel" => integer!(default_if_omitted(fabric["vpc_peer_link_port_channel"], 998), "vpc_peer_link_port_channel", 1..4096)
|
|
186
|
+
)
|
|
187
|
+
if result["vpc_peer_keepalive_destination"] == result["vpc_peer_keepalive_source"]
|
|
188
|
+
raise self.class::Error, "vPC keepalive source and destination must be different"
|
|
189
|
+
end
|
|
190
|
+
virtual_destination = fabric["vpc_virtual_peer_link_destination"]
|
|
191
|
+
virtual_source = fabric["vpc_virtual_peer_link_source"]
|
|
192
|
+
if provided?(virtual_destination) != provided?(virtual_source)
|
|
193
|
+
raise self.class::Error, "vPC virtual peer-link source and destination must be provided together"
|
|
194
|
+
end
|
|
195
|
+
return unless provided?(virtual_destination)
|
|
196
|
+
|
|
197
|
+
result.merge!(
|
|
198
|
+
"vpc_virtual_peer_link_enabled" => true,
|
|
199
|
+
"vpc_virtual_peer_link_destination" => ipv4!(virtual_destination, "vpc_virtual_peer_link_destination"),
|
|
200
|
+
"vpc_virtual_peer_link_source" => ipv4!(virtual_source, "vpc_virtual_peer_link_source")
|
|
201
|
+
)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def bootstrap_interface_list!(value, path, max:)
|
|
205
|
+
unless value.is_a?(Array) && value.size.between?(1, max)
|
|
206
|
+
raise self.class::Error, "#{path} must contain 1 to #{max} values"
|
|
207
|
+
end
|
|
208
|
+
interfaces = value.map.with_index { |item, index| interface!(item, "#{path}[#{index}]") }
|
|
209
|
+
raise self.class::Error, "#{path} must be unique" unless interfaces.map(&:downcase).uniq.size == interfaces.size
|
|
210
|
+
interfaces
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def bootstrap_ipv4_list!(value, path, max:)
|
|
214
|
+
unless value.is_a?(Array) && value.size.between?(1, max)
|
|
215
|
+
raise self.class::Error, "#{path} must contain 1 to #{max} values"
|
|
216
|
+
end
|
|
217
|
+
addresses = value.map.with_index { |item, index| ipv4!(item, "#{path}[#{index}]") }
|
|
218
|
+
raise self.class::Error, "#{path} must be unique" unless addresses.uniq.size == addresses.size
|
|
219
|
+
addresses
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Adapted from Motor Netconf scenario definitions and safe template dialect.
|
|
4
|
+
require_relative "support"
|
|
5
|
+
|
|
6
|
+
module Netstack
|
|
7
|
+
module Configuration
|
|
8
|
+
class Catalog
|
|
9
|
+
SCENARIOS = Values.copy([
|
|
10
|
+
{ key: "bootstrap", group: "device", default_vendor: "h3c", vendors: %w[h3c cisco], templates: { "h3c" => "h3c-bootstrap.erb", "cisco" => "cisco-bootstrap.erb" } },
|
|
11
|
+
{ key: "h3c-irf", group: "device", default_vendor: "h3c", vendors: %w[h3c], templates: { "h3c" => "h3c-irf.erb" } },
|
|
12
|
+
{ key: "distributed-gateway", group: "network", default_vendor: "cisco", vendors: %w[cisco], templates: { "cisco" => "cisco-distributed-gateway.erb" } },
|
|
13
|
+
{ key: "cisco-n9k-vpc", group: "network", default_vendor: "cisco", vendors: %w[cisco], templates: { "cisco" => "cisco-n9k-vpc.erb" } },
|
|
14
|
+
{ key: "server-port", group: "network", default_vendor: "h3c", vendors: %w[h3c cisco], templates: { "h3c" => "h3c-server-port.erb", "cisco" => "cisco-server-port.erb" } },
|
|
15
|
+
{ key: "hillstone-policy", group: "network", default_vendor: "hillstone", vendors: %w[hillstone], templates: { "hillstone" => "hillstone-policy.erb" } },
|
|
16
|
+
{ key: "load-balancer", group: "application", default_vendor: "radware", vendors: %w[radware], templates: { "radware" => "radware-load-balancer.erb" } },
|
|
17
|
+
{ key: "ipsec-vpn", group: "security", default_vendor: "paloalto", vendors: %w[paloalto hillstone], templates: { "paloalto" => "paloalto-ipsec-vpn.erb", "hillstone" => "hillstone-ipsec-vpn.erb" } }
|
|
18
|
+
])
|
|
19
|
+
VARIABLES = Values.copy({
|
|
20
|
+
"bootstrap" => %w[
|
|
21
|
+
hostname device_role stp_priority mgmt mgmt_ip mgmt_mask mgmt_gateway snmp_location system
|
|
22
|
+
access_interface_start access_interface_end uplink_interfaces h3c_uplinks
|
|
23
|
+
access_auth_enabled access_auth_domain access_auth_vlan access_auth_interface_start access_auth_interface_end
|
|
24
|
+
underlay_enabled loopback loopback_ip ospf_process_id ospf_area fabric_mtu fabric_links
|
|
25
|
+
overlay_enabled local_as remote_as overlay_neighbors bgp_neighbors vtep_loopback vtep_secondary
|
|
26
|
+
nve_enabled route_reflector external_bgp_neighbors vtep_secondary_enabled advertise_pip
|
|
27
|
+
vpc_enabled vpc_domain_id vpc_role_priority
|
|
28
|
+
vpc_peer_keepalive_destination vpc_peer_keepalive_source vpc_virtual_peer_link_enabled
|
|
29
|
+
vpc_virtual_peer_link_destination vpc_virtual_peer_link_source vpc_peer_link_port_channel
|
|
30
|
+
],
|
|
31
|
+
"h3c-irf" => %w[port1 port2 port3 port4 mad_port1 mad_port2 mad_vlan mad_subnet mad_mask mad_member1_ip mad_member2_ip],
|
|
32
|
+
"distributed-gateway" => %w[gateways],
|
|
33
|
+
"cisco-n9k-vpc" => %w[vpc_domain_id primary_role_priority secondary_role_priority primary_keepalive_ip secondary_keepalive_ip primary_virtual_peer_link_ip secondary_virtual_peer_link_ip vpc_peer_link_port_channel infra_vlan fabric_interfaces],
|
|
34
|
+
"server-port" => %w[stack_mode allocations aggregates],
|
|
35
|
+
"hillstone-policy" => %w[policies address_objects service_objects service_groups],
|
|
36
|
+
"load-balancer" => %w[next_hop route_metric services real_servers groups virtuals],
|
|
37
|
+
"ipsec-vpn" => %w[site_name tunnel local_interface local_address local_ip remote_ip add_route virtual_router route_metric dpd_interval dpd_retry hillstone_sa_index tunnel_zone preshared_key local_subnets remote_subnets ike_version ikev1 ikev2 ike_encryption ike_authentication ike_dh_group ike_lifetime ipsec_encryption ipsec_authentication ipsec_lifetime hillstone_ike_encryption hillstone_ipsec_encryption hillstone_ikev2_encryption hillstone_ipsec_v2_encryption hillstone_ike_hash hillstone_ipsec_hash hillstone_ike_group proxy_ids routes]
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
class << self
|
|
41
|
+
def all = SCENARIOS.deep_dup
|
|
42
|
+
|
|
43
|
+
def fetch(key)
|
|
44
|
+
SCENARIOS.find { |scenario| scenario.fetch(:key) == key.to_s } || raise(KeyError, "unknown scenario")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def template_key(scenario, vendor)
|
|
48
|
+
fetch(scenario).fetch(:templates).fetch(vendor.to_s) { raise KeyError, "unsupported vendor" }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def template_keys = SCENARIOS.flat_map { |scenario| scenario.fetch(:templates).values }.uniq
|
|
52
|
+
|
|
53
|
+
def variables_for_template(key)
|
|
54
|
+
scenario = SCENARIOS.find { |item| item.fetch(:templates).value?(key) } || raise(KeyError, "unknown template")
|
|
55
|
+
VARIABLES.fetch(scenario.fetch(:key))
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "support"
|
|
4
|
+
|
|
5
|
+
module Netstack
|
|
6
|
+
# 经过校验的场景输入、受限模板和敏感私有工件。
|
|
7
|
+
module Configuration
|
|
8
|
+
# 在本地生成工件;不向设备下发配置。
|
|
9
|
+
# 初始化批次必须全部校验完成,才渲染任何输出工件。
|
|
10
|
+
class Generator
|
|
11
|
+
def self.call(**options) = new.call(**options)
|
|
12
|
+
|
|
13
|
+
def initialize(store: TemplateStore.new, clock: -> { Time.now.utc })
|
|
14
|
+
@store = store
|
|
15
|
+
@clock = clock
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# 批量初始化返回可独立引用的工件数组。
|
|
19
|
+
# 归档、邮件、加密和保留策略由宿主负责。
|
|
20
|
+
def call(scenario:, vendor:, input:, system_config: {}, settings: {})
|
|
21
|
+
values = Support.object(input)
|
|
22
|
+
now = @clock.call
|
|
23
|
+
if scenario.to_s == "bootstrap" && values.key?("devices")
|
|
24
|
+
return bootstrap_batch(vendor: vendor, input: values, system_config: system_config, settings: settings, now: now)
|
|
25
|
+
end
|
|
26
|
+
normalized = Validator.call(scenario: scenario, vendor: vendor, input: values, system_config: system_config, settings: settings)
|
|
27
|
+
render(scenario.to_s, vendor.to_s, normalized, now)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def bootstrap_batch(vendor:, input:, system_config:, settings:, now:)
|
|
33
|
+
raise InputError, "unexpected bootstrap batch fields" unless (input.keys - %w[devices system]).empty?
|
|
34
|
+
devices = input.fetch("devices")
|
|
35
|
+
unless devices.is_a?(Array) && devices.size.between?(1, Validator::MAX_ROWS)
|
|
36
|
+
raise InputError, "devices must contain 1 to #{Validator::MAX_ROWS} rows"
|
|
37
|
+
end
|
|
38
|
+
normalized = devices.map do |device|
|
|
39
|
+
row = Support.object(device, name: "bootstrap device")
|
|
40
|
+
raise InputError, "batch system overrides belong at the batch level" if row.key?("system")
|
|
41
|
+
row["system"] = input["system"] if input.key?("system")
|
|
42
|
+
Validator.call(scenario: "bootstrap", vendor: vendor, input: row, system_config: system_config, settings: settings)
|
|
43
|
+
end
|
|
44
|
+
hostnames = normalized.map { |row| row.fetch("hostname").downcase }
|
|
45
|
+
addresses = normalized.map { |row| row.fetch("mgmt").split("/").first }
|
|
46
|
+
raise InputError, "device hostnames and management addresses must be unique" unless hostnames.uniq.size == hostnames.size && addresses.uniq.size == addresses.size
|
|
47
|
+
|
|
48
|
+
normalized.map { |row| render("bootstrap", vendor.to_s, row, now) }.freeze
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def render(scenario, vendor, normalized, now)
|
|
52
|
+
key = Catalog.template_key(scenario, vendor)
|
|
53
|
+
system = normalized.delete("system") || {}
|
|
54
|
+
data = Payload.call(scenario: scenario, input: normalized, system_config: system, now: now)
|
|
55
|
+
content = TemplateManager.new(key, data, store: @store).generate
|
|
56
|
+
prefix = normalized["hostname"] || normalized["site_name"] || "#{vendor}-#{scenario}"
|
|
57
|
+
filename = "#{prefix}-#{now.strftime("%Y%m%d%H%M%S")}.txt"
|
|
58
|
+
options = { scenario: scenario, vendor: vendor, created_at: now, template_sha256: @store.metadata(key).fetch(:sha256) }
|
|
59
|
+
sections = split_sections(scenario, content).to_h do |name, text|
|
|
60
|
+
[name, Artifact.new(filename: "#{prefix}-#{name}.txt", content: text, **options)]
|
|
61
|
+
end
|
|
62
|
+
Artifact.new(filename: filename, content: content, sections: sections, **options)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def split_sections(scenario, content)
|
|
66
|
+
markers = case scenario
|
|
67
|
+
when "cisco-n9k-vpc" then { "primary" => "! Primary switch", "secondary" => "! Secondary switch" }
|
|
68
|
+
when "h3c-irf" then { "master" => "# Stage 1 - master", "standby-renumber" => "# Stage 2 - standby renumber",
|
|
69
|
+
"standby-links" => "# Stage 3 - standby links", "mad" => "# Stage 4 - BFD MAD" }
|
|
70
|
+
else return {}
|
|
71
|
+
end
|
|
72
|
+
positions = markers.map do |name, marker|
|
|
73
|
+
matches = content.enum_for(:scan, /^#{Regexp.escape(marker)}$/).map { Regexp.last_match.begin(0) }
|
|
74
|
+
raise InputError, "multi-target template must preserve unique section markers" unless matches.one?
|
|
75
|
+
|
|
76
|
+
[name, matches.first]
|
|
77
|
+
end
|
|
78
|
+
raise InputError, "configuration sections are out of order" unless positions.map(&:last) == positions.map(&:last).sort
|
|
79
|
+
|
|
80
|
+
positions.each_with_index.to_h do |(name, start), index|
|
|
81
|
+
finish = positions[index + 1]&.last || content.length
|
|
82
|
+
[name, content[start...finish]]
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Adapted from Motor Netconf scenario definitions and safe template dialect.
|
|
4
|
+
require_relative "support"
|
|
5
|
+
|
|
6
|
+
module Netstack
|
|
7
|
+
module Configuration
|
|
8
|
+
# Defines the HTTP input shape separately from template output variables.
|
|
9
|
+
class InputContract
|
|
10
|
+
EMPTY_FIELDS = [].freeze
|
|
11
|
+
|
|
12
|
+
SYSTEM_FIELDS = Values.copy([
|
|
13
|
+
:domain_name, :timezone, :management_vlan, :snmp_community,
|
|
14
|
+
:tacacs_secret, :radius_secret, :base_security_enabled,
|
|
15
|
+
:disable_telnet, :disable_http, :enable_bpdu_protection,
|
|
16
|
+
:password_min_length, :vty_timeout_minutes,
|
|
17
|
+
{ ntp_servers: [], dns_servers: [], syslog_servers: [],
|
|
18
|
+
tacacs_servers: [], radius_servers: [], ssh_acl_cidrs: [],
|
|
19
|
+
local_users: %i[username password role] }
|
|
20
|
+
])
|
|
21
|
+
|
|
22
|
+
BOOTSTRAP_DEVICE_FIELDS = Values.copy([
|
|
23
|
+
:hostname, :device_role, :mgmt, :mgmt_gateway, :snmp_location,
|
|
24
|
+
:access_auth_enabled, :access_auth_domain, :access_auth_vlan,
|
|
25
|
+
:access_auth_interface_start, :access_auth_interface_end,
|
|
26
|
+
:access_interface_start, :access_interface_end,
|
|
27
|
+
:underlay_enabled, :loopback, :ospf_process_id, :ospf_area,
|
|
28
|
+
:fabric_mtu, :overlay_enabled, :local_as, :remote_as,
|
|
29
|
+
:vtep_loopback, :vtep_secondary, :vpc_enabled, :vpc_domain_id,
|
|
30
|
+
:vpc_role_priority, :vpc_peer_keepalive_destination,
|
|
31
|
+
:vpc_peer_keepalive_source, :vpc_virtual_peer_link_destination,
|
|
32
|
+
:vpc_virtual_peer_link_source, :vpc_peer_link_port_channel,
|
|
33
|
+
{ uplink_interfaces: [], overlay_neighbors: [],
|
|
34
|
+
fabric_links: %i[interface address description],
|
|
35
|
+
external_bgp_neighbors: %i[address remote_as description] }
|
|
36
|
+
])
|
|
37
|
+
|
|
38
|
+
SCHEMAS = Values.copy({
|
|
39
|
+
"bootstrap" => [
|
|
40
|
+
*BOOTSTRAP_DEVICE_FIELDS,
|
|
41
|
+
{ system: SYSTEM_FIELDS, devices: BOOTSTRAP_DEVICE_FIELDS }
|
|
42
|
+
],
|
|
43
|
+
"h3c-irf" => %i[port1 port2 port3 port4 mad_port1 mad_port2 mad_vlan mad_subnet],
|
|
44
|
+
"distributed-gateway" => [ { gateways: %i[vlan subnet gateway vni name mtu vrf acl] } ],
|
|
45
|
+
"cisco-n9k-vpc" => [
|
|
46
|
+
:vpc_domain_id, :primary_role_priority, :secondary_role_priority,
|
|
47
|
+
:primary_keepalive_ip, :secondary_keepalive_ip,
|
|
48
|
+
:primary_virtual_peer_link_ip, :secondary_virtual_peer_link_ip,
|
|
49
|
+
:vpc_peer_link_port_channel, :infra_vlan, { fabric_interfaces: [] }
|
|
50
|
+
],
|
|
51
|
+
"server-port" => [
|
|
52
|
+
:stack_mode,
|
|
53
|
+
{ allocations: %i[local_iface local_description remote remote_iface vlan port_channel aggregate_id flag] }
|
|
54
|
+
],
|
|
55
|
+
"hillstone-policy" => [
|
|
56
|
+
{ policies: [
|
|
57
|
+
:description, :src_zone, :dst_zone, :protocol, :action, :log,
|
|
58
|
+
{ src_addresses: [], dst_addresses: [], services: [] }
|
|
59
|
+
] }
|
|
60
|
+
],
|
|
61
|
+
"load-balancer" => [
|
|
62
|
+
{ services: [
|
|
63
|
+
:flag, :virtual_ip, :service_port, :proxy_ip, :app_port,
|
|
64
|
+
:timeout, :group_name, :service_type, :health_check,
|
|
65
|
+
:client_ip_persistence, { real_ips: [] }
|
|
66
|
+
] }
|
|
67
|
+
],
|
|
68
|
+
"ipsec-vpn" => [
|
|
69
|
+
:site_name, :tunnel, :local_interface, :local_address, :remote_ip,
|
|
70
|
+
:add_route, :preshared_key, :ike_version, :ike_encryption,
|
|
71
|
+
:ike_authentication, :ike_dh_group, :ike_lifetime,
|
|
72
|
+
:ipsec_encryption, :ipsec_authentication, :ipsec_lifetime,
|
|
73
|
+
:virtual_router, :route_metric, :dpd_interval, :dpd_retry, :hillstone_sa_index, :tunnel_zone,
|
|
74
|
+
{ local_subnets: [], remote_subnets: [] }
|
|
75
|
+
]
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
# Unknown scenarios receive no input fields and are rejected by the catalog.
|
|
79
|
+
def self.fields_for(scenario) = SCHEMAS.fetch(scenario.to_s, EMPTY_FIELDS)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|