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,566 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Adapted from Motor Netconf scenario definitions and safe template dialect.
|
|
4
|
+
require_relative "support"
|
|
5
|
+
|
|
6
|
+
require "ipaddr"
|
|
7
|
+
require "socket"
|
|
8
|
+
require_relative "bootstrap/validator"
|
|
9
|
+
|
|
10
|
+
module Netstack
|
|
11
|
+
module Configuration
|
|
12
|
+
class Validator
|
|
13
|
+
include Bootstrap::Validator
|
|
14
|
+
|
|
15
|
+
Error = Class.new(Netstack::InputError)
|
|
16
|
+
MAX_ROWS = 64
|
|
17
|
+
|
|
18
|
+
def self.call(scenario:, vendor:, input:, system_config: {}, settings: {})
|
|
19
|
+
new(scenario:, vendor:, input:, system_config:, settings:).call
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(scenario:, vendor:, input:, system_config: {}, settings: {})
|
|
23
|
+
@scenario = scenario.to_s
|
|
24
|
+
@vendor = vendor.to_s
|
|
25
|
+
@input = object!(input, "input")
|
|
26
|
+
@system_config = object!(system_config, "system_config")
|
|
27
|
+
@settings = object!(settings, "settings")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def call
|
|
31
|
+
Catalog.template_key(@scenario, @vendor)
|
|
32
|
+
send("validate_#{@scenario.tr("-", "_")}")
|
|
33
|
+
rescue KeyError => exception
|
|
34
|
+
raise Error, exception.message
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def inspect = "#<#{self.class.name}>"
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
# Only omitted values and blank form text select defaults. Other types must
|
|
42
|
+
# reach the field validator instead of becoming a different configuration.
|
|
43
|
+
def provided?(value) = !value.nil? && (!value.is_a?(String) || value.present?)
|
|
44
|
+
|
|
45
|
+
def default_if_omitted(value, fallback) = provided?(value) ? value : fallback
|
|
46
|
+
|
|
47
|
+
def validate_bootstrap
|
|
48
|
+
common = take!(%w[hostname device_role mgmt mgmt_gateway snmp_location system access_auth_enabled access_auth_domain access_auth_vlan access_auth_interface_start access_auth_interface_end])
|
|
49
|
+
result = {
|
|
50
|
+
"hostname" => hostname!(common["hostname"], "hostname"),
|
|
51
|
+
"device_role" => enum!((default_if_omitted(common["device_role"], bootstrap_roles.first)).to_s.downcase, "device_role", bootstrap_roles),
|
|
52
|
+
"mgmt" => management_cidr!(common["mgmt"], "mgmt"),
|
|
53
|
+
"snmp_location" => description!(common["snmp_location"], "snmp_location")
|
|
54
|
+
}
|
|
55
|
+
validate_bootstrap_gateway!(result, common["mgmt_gateway"])
|
|
56
|
+
access_auth_enabled = @vendor == "h3c" && (common["access_auth_enabled"].nil? ? false : boolean!(common["access_auth_enabled"], "access_auth_enabled"))
|
|
57
|
+
if access_auth_enabled && common["access_auth_vlan"].blank?
|
|
58
|
+
raise Error, "access_auth_vlan is required when admission control is enabled"
|
|
59
|
+
end
|
|
60
|
+
result["system"] = system_configuration!(common["system"].nil? ? {} : common["system"], access_auth_enabled: access_auth_enabled)
|
|
61
|
+
return validate_h3c_bootstrap!(result, common, access_auth_enabled) if @vendor == "h3c"
|
|
62
|
+
|
|
63
|
+
validate_cisco_bootstrap!(result)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def validate_h3c_irf
|
|
67
|
+
values = take!(%w[port1 port2 port3 port4 mad_port1 mad_port2 mad_vlan mad_subnet])
|
|
68
|
+
result = {}
|
|
69
|
+
%w[port1 port2 port3 port4 mad_port1 mad_port2].each { |field| result[field] = interface!(values[field], field) }
|
|
70
|
+
result["mad_vlan"] = integer!(values["mad_vlan"], "mad_vlan", 1..4095)
|
|
71
|
+
result["mad_subnet"] = cidr!(values["mad_subnet"], "mad_subnet", network: true)
|
|
72
|
+
raise Error, "mad_subnet prefix must be between 1 and 30" unless result["mad_subnet"].split("/").last.to_i.between?(1, 30)
|
|
73
|
+
interfaces = result.values_at(*%w[port1 port2 port3 port4 mad_port1 mad_port2])
|
|
74
|
+
raise Error, "IRF and MAD interfaces must be unique" unless interfaces.map(&:downcase).uniq.size == 6
|
|
75
|
+
finish!(result)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def validate_distributed_gateway
|
|
79
|
+
values = take!(%w[gateways])
|
|
80
|
+
gateways = rows!(values["gateways"], "gateways").map.with_index do |row, index|
|
|
81
|
+
row = exact!(row, %w[vlan subnet gateway vni name mtu vrf acl], "gateways[#{index}]")
|
|
82
|
+
vlan = integer!(row["vlan"], "gateways[#{index}].vlan", 1..4094)
|
|
83
|
+
subnet = cidr!(row["subnet"], "gateways[#{index}].subnet", network: true)
|
|
84
|
+
raise Error, "gateways[#{index}].subnet prefix must be between 1 and 30" unless subnet.split("/").last.to_i.between?(1, 30)
|
|
85
|
+
gateway = provided?(row["gateway"]) ? ipv4!(row["gateway"], "gateways[#{index}].gateway") : last_usable(subnet)
|
|
86
|
+
raise Error, "gateways[#{index}].gateway must belong to subnet" unless IPAddr.new(subnet).include?(gateway)
|
|
87
|
+
subnet_range = IPAddr.new(subnet).to_range
|
|
88
|
+
if [ subnet_range.first.to_s, subnet_range.last.to_s ].include?(gateway)
|
|
89
|
+
raise Error, "gateways[#{index}].gateway must be a usable host address"
|
|
90
|
+
end
|
|
91
|
+
{ "vlan" => vlan, "subnet" => subnet, "gateway" => gateway,
|
|
92
|
+
"vni" => provided?(row["vni"]) ? integer!(row["vni"], "gateways[#{index}].vni", 1..16_777_215) : 10_000 + vlan,
|
|
93
|
+
"name" => optional_description(row["name"], "gateways[#{index}].name"),
|
|
94
|
+
"mtu" => provided?(row["mtu"]) ? integer!(row["mtu"], "gateways[#{index}].mtu", 1280..9216) : 9216,
|
|
95
|
+
"vrf" => identifier!(default_if_omitted(row["vrf"], "VXLAN"), "gateways[#{index}].vrf"),
|
|
96
|
+
"acl" => optional_identifier(row["acl"], "gateways[#{index}].acl") }
|
|
97
|
+
end
|
|
98
|
+
raise Error, "gateway VLAN and VNI values must be unique" unless gateways.map { |row| row["vlan"] }.uniq.size == gateways.size && gateways.map { |row| row["vni"] }.uniq.size == gateways.size
|
|
99
|
+
finish!("gateways" => gateways)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def validate_cisco_n9k_vpc
|
|
103
|
+
values = take!(%w[
|
|
104
|
+
vpc_domain_id primary_role_priority secondary_role_priority
|
|
105
|
+
primary_keepalive_ip secondary_keepalive_ip primary_virtual_peer_link_ip
|
|
106
|
+
secondary_virtual_peer_link_ip vpc_peer_link_port_channel infra_vlan fabric_interfaces
|
|
107
|
+
])
|
|
108
|
+
primary_keepalive = ipv4!(values["primary_keepalive_ip"], "primary_keepalive_ip")
|
|
109
|
+
secondary_keepalive = ipv4!(values["secondary_keepalive_ip"], "secondary_keepalive_ip")
|
|
110
|
+
raise Error, "vPC keepalive addresses must be different" if primary_keepalive == secondary_keepalive
|
|
111
|
+
|
|
112
|
+
primary_virtual = ipv4!(values["primary_virtual_peer_link_ip"], "primary_virtual_peer_link_ip")
|
|
113
|
+
secondary_virtual = ipv4!(values["secondary_virtual_peer_link_ip"], "secondary_virtual_peer_link_ip")
|
|
114
|
+
addresses = [ primary_keepalive, secondary_keepalive, primary_virtual, secondary_virtual ]
|
|
115
|
+
raise Error, "vPC endpoint addresses must be unique" unless addresses.uniq.size == addresses.size
|
|
116
|
+
|
|
117
|
+
primary_priority = integer!(default_if_omitted(values["primary_role_priority"], 100), "primary_role_priority", 1..65_535)
|
|
118
|
+
secondary_priority = integer!(default_if_omitted(values["secondary_role_priority"], 200), "secondary_role_priority", 1..65_535)
|
|
119
|
+
raise Error, "vPC role priorities must be different" if primary_priority == secondary_priority
|
|
120
|
+
|
|
121
|
+
raw_interfaces = values["fabric_interfaces"]
|
|
122
|
+
raw_interfaces = raw_interfaces.lines if raw_interfaces.is_a?(String)
|
|
123
|
+
unless raw_interfaces.is_a?(Array) && raw_interfaces.all? { |value| value.is_a?(String) }
|
|
124
|
+
raise Error, "fabric_interfaces must be text lines or an array of strings"
|
|
125
|
+
end
|
|
126
|
+
raw_interfaces = raw_interfaces.map(&:strip).reject(&:empty?)
|
|
127
|
+
unless raw_interfaces.size.between?(1, 16)
|
|
128
|
+
raise Error, "fabric_interfaces must contain 1 to 16 values"
|
|
129
|
+
end
|
|
130
|
+
interfaces = raw_interfaces.map.with_index do |interface, index|
|
|
131
|
+
interface!(interface, "fabric_interfaces[#{index}]")
|
|
132
|
+
end
|
|
133
|
+
raise Error, "fabric interfaces must be unique" unless interfaces.map(&:downcase).uniq.size == interfaces.size
|
|
134
|
+
|
|
135
|
+
finish!(
|
|
136
|
+
"vpc_domain_id" => integer!(values["vpc_domain_id"], "vpc_domain_id", 1..1000),
|
|
137
|
+
"primary_role_priority" => primary_priority,
|
|
138
|
+
"secondary_role_priority" => secondary_priority,
|
|
139
|
+
"primary_keepalive_ip" => primary_keepalive,
|
|
140
|
+
"secondary_keepalive_ip" => secondary_keepalive,
|
|
141
|
+
"primary_virtual_peer_link_ip" => primary_virtual,
|
|
142
|
+
"secondary_virtual_peer_link_ip" => secondary_virtual,
|
|
143
|
+
"vpc_peer_link_port_channel" => integer!(default_if_omitted(values["vpc_peer_link_port_channel"], 999), "vpc_peer_link_port_channel", 1..4096),
|
|
144
|
+
"infra_vlan" => integer!(default_if_omitted(values["infra_vlan"], 3967), "infra_vlan", 1..4094),
|
|
145
|
+
"fabric_interfaces" => interfaces
|
|
146
|
+
)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def validate_server_port
|
|
150
|
+
values = take!(%w[stack_mode allocations])
|
|
151
|
+
mode = enum!(default_if_omitted(values["stack_mode"], "standalone"), "stack_mode", @vendor == "h3c" ? %w[standalone irf] : %w[standalone vpc])
|
|
152
|
+
allocations = rows!(values["allocations"], "allocations").map.with_index do |row, index|
|
|
153
|
+
row = exact!(row, %w[local_iface local_description remote remote_iface vlan port_channel aggregate_id flag], "allocations[#{index}]")
|
|
154
|
+
aggregated = boolean!(row["port_channel"], "allocations[#{index}].port_channel")
|
|
155
|
+
local_iface = interface!(row["local_iface"], "allocations[#{index}].local_iface")
|
|
156
|
+
remote = hostname!(row["remote"], "allocations[#{index}].remote")
|
|
157
|
+
remote_iface = interface!(row["remote_iface"], "allocations[#{index}].remote_iface")
|
|
158
|
+
aggregate_id = integer!(row["aggregate_id"], "allocations[#{index}].aggregate_id", 1..4096) if aggregated && provided?(row["aggregate_id"])
|
|
159
|
+
{ "local_iface" => local_iface,
|
|
160
|
+
"local_description" => description!(default_if_omitted(row["local_description"], automatic_local_description(remote, remote_iface, aggregated)), "allocations[#{index}].local_description"),
|
|
161
|
+
"remote" => remote,
|
|
162
|
+
"remote_iface" => remote_iface,
|
|
163
|
+
"vlan" => vlan_value!(row["vlan"], "allocations[#{index}].vlan"),
|
|
164
|
+
"port_channel" => aggregated,
|
|
165
|
+
"aggregate_id" => aggregate_id,
|
|
166
|
+
"flag" => optional_description(row["flag"], "allocations[#{index}].flag") }
|
|
167
|
+
end
|
|
168
|
+
assign_automatic_aggregate_ids!(allocations, mode)
|
|
169
|
+
raise Error, "local interfaces must be unique" unless allocations.map { |row| row["local_iface"].downcase }.uniq.size == allocations.size
|
|
170
|
+
aggregate_groups = allocations.select { |row| row["port_channel"] }.group_by { |row| row["aggregate_id"] }
|
|
171
|
+
unless aggregate_groups.values.all? { |rows| rows.map { |row| [ row["remote"].downcase, row["vlan"] ] }.uniq.one? }
|
|
172
|
+
raise Error, "ports in the same aggregate must use the same remote device and VLAN"
|
|
173
|
+
end
|
|
174
|
+
if mode == "vpc"
|
|
175
|
+
unless aggregate_groups.values.all?(&:one?)
|
|
176
|
+
raise Error, "vPC links must use unique aggregate identifiers"
|
|
177
|
+
end
|
|
178
|
+
elsif !aggregate_groups.values.all? { |rows| rows.size == 2 }
|
|
179
|
+
raise Error, "non-vPC aggregates must contain exactly two links"
|
|
180
|
+
end
|
|
181
|
+
finish!(
|
|
182
|
+
"stack_mode" => mode, "allocations" => allocations
|
|
183
|
+
)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def validate_hillstone_policy
|
|
187
|
+
values = take!(%w[policies])
|
|
188
|
+
policies = rows!(values["policies"], "policies").map.with_index do |row, index|
|
|
189
|
+
path = "policies[#{index}]"
|
|
190
|
+
row = exact!(row, %w[description src_zone src_addresses dst_zone dst_addresses protocol services action log], path)
|
|
191
|
+
|
|
192
|
+
{
|
|
193
|
+
"description" => policy_description!(row["description"], "#{path}.description"),
|
|
194
|
+
"src_zone" => policy_zone!(default_if_omitted(row["src_zone"], "any"), "#{path}.src_zone"),
|
|
195
|
+
"src_addresses" => policy_address_list!(row["src_addresses"], "#{path}.src_addresses"),
|
|
196
|
+
"dst_zone" => policy_zone!(default_if_omitted(row["dst_zone"], "any"), "#{path}.dst_zone"),
|
|
197
|
+
"dst_addresses" => policy_address_list!(row["dst_addresses"], "#{path}.dst_addresses"),
|
|
198
|
+
"protocol" => enum!(row["protocol"].to_s.downcase, "#{path}.protocol", %w[tcp udp]),
|
|
199
|
+
"services" => policy_service_list!(row["services"], "#{path}.services"),
|
|
200
|
+
"action" => enum!((default_if_omitted(row["action"], "permit")).to_s.downcase, "#{path}.action", %w[permit deny]),
|
|
201
|
+
"log" => row["log"].nil? ? true : boolean!(row["log"], "#{path}.log")
|
|
202
|
+
}
|
|
203
|
+
end
|
|
204
|
+
finish!("policies" => policies)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def validate_load_balancer
|
|
208
|
+
values = take!(%w[services])
|
|
209
|
+
services = rows!(values["services"], "services").map.with_index do |row, index|
|
|
210
|
+
row = exact!(row, %w[flag real_ips virtual_ip service_port proxy_ip app_port timeout group_name service_type health_check client_ip_persistence], "services[#{index}]")
|
|
211
|
+
real_ips = string_list!(row["real_ips"], "services[#{index}].real_ips").map.with_index do |ip, ip_index|
|
|
212
|
+
ipv4!(ip, "services[#{index}].real_ips[#{ip_index}]")
|
|
213
|
+
end
|
|
214
|
+
flag = identifier!(row["flag"], "services[#{index}].flag")
|
|
215
|
+
service_name = flag
|
|
216
|
+
virtual_ip = ipv4!(row["virtual_ip"], "services[#{index}].virtual_ip")
|
|
217
|
+
service_port = integer!(row["service_port"], "services[#{index}].service_port", 1..65_535)
|
|
218
|
+
app_port = provided?(row["app_port"]) ? integer!(row["app_port"], "services[#{index}].app_port", 1..65_535) : service_port
|
|
219
|
+
{ "flag" => flag, "service_name" => service_name, "real_ips" => real_ips,
|
|
220
|
+
"virtual_ip" => virtual_ip, "service_port" => service_port,
|
|
221
|
+
"proxy_ip" => ipv4!(default_if_omitted(row["proxy_ip"], virtual_ip), "services[#{index}].proxy_ip"),
|
|
222
|
+
"app_port" => app_port,
|
|
223
|
+
"timeout" => provided?(row["timeout"]) ? integer!(row["timeout"], "services[#{index}].timeout", 1..86_400) : 30,
|
|
224
|
+
"group_name" => provided?(row["group_name"]) ? identifier!(row["group_name"], "services[#{index}].group_name") : automatic_group_name(flag, app_port),
|
|
225
|
+
"service_type" => enum!(default_if_omitted(row["service_type"], "basic-slb"), "services[#{index}].service_type", %w[basic-slb http https]),
|
|
226
|
+
"health_check" => optional_identifier(row["health_check"], "services[#{index}].health_check"),
|
|
227
|
+
"client_ip_persistence" => row["client_ip_persistence"].nil? ? false : boolean!(row["client_ip_persistence"], "services[#{index}].client_ip_persistence") }
|
|
228
|
+
end
|
|
229
|
+
keys = services.map { |row| [ row["virtual_ip"], row["service_port"] ] }
|
|
230
|
+
raise Error, "VIP address and port pairs must be unique" unless keys.uniq.size == keys.size
|
|
231
|
+
groups = services.group_by { |row| row["group_name"].downcase }
|
|
232
|
+
unless groups.values.all? { |rows| rows.map { |row| [ row["real_ips"], row["health_check"] ] }.uniq.one? }
|
|
233
|
+
raise Error, "services using the same group_name must use the same real_ips and health_check"
|
|
234
|
+
end
|
|
235
|
+
finish!(
|
|
236
|
+
"next_hop" => ipv4!(@settings.fetch("next_hop") { raise Error, "settings.next_hop is required" }, "settings.next_hop"),
|
|
237
|
+
"route_metric" => integer!(@settings.fetch("route_metric", 1), "settings.route_metric", 1..65_535),
|
|
238
|
+
"services" => services
|
|
239
|
+
)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def validate_ipsec_vpn
|
|
243
|
+
values = take!(%w[site_name tunnel local_interface local_address remote_ip add_route preshared_key local_subnets remote_subnets ike_version ike_encryption ike_authentication ike_dh_group ike_lifetime ipsec_encryption ipsec_authentication ipsec_lifetime virtual_router route_metric dpd_interval dpd_retry hillstone_sa_index tunnel_zone])
|
|
244
|
+
add_route = values["add_route"].nil? || boolean!(values["add_route"], "add_route")
|
|
245
|
+
encryption_options = @vendor == "hillstone" ? %w[aes-256-cbc 3des] : %w[aes-256-gcm aes-256-cbc 3des]
|
|
246
|
+
dh_group_options = @vendor == "hillstone" ? %w[group5 group2] : %w[group19 group14 group5 group2]
|
|
247
|
+
result = {
|
|
248
|
+
"site_name" => identifier!(values["site_name"], "site_name"),
|
|
249
|
+
"tunnel" => interface!(values["tunnel"], "tunnel"), "local_interface" => interface!(values["local_interface"], "local_interface"),
|
|
250
|
+
"local_address" => cidr!(values["local_address"], "local_address", host: true),
|
|
251
|
+
"remote_ip" => ipv4!(values["remote_ip"], "remote_ip"),
|
|
252
|
+
"preshared_key" => secret!(values["preshared_key"], "preshared_key"),
|
|
253
|
+
"local_subnets" => string_list!(values["local_subnets"], "local_subnets").map { |cidr| cidr!(cidr, "local_subnets", network: true) },
|
|
254
|
+
"remote_subnets" => string_list!(values["remote_subnets"], "remote_subnets").map { |cidr| cidr!(cidr, "remote_subnets", network: true) },
|
|
255
|
+
"ike_version" => enum!(values["ike_version"], "ike_version", %w[ikev1 ikev2]),
|
|
256
|
+
"ike_encryption" => enum!(values["ike_encryption"], "ike_encryption", encryption_options),
|
|
257
|
+
"ike_authentication" => enum!(values["ike_authentication"], "ike_authentication", %w[sha256 sha1 md5]),
|
|
258
|
+
"ike_dh_group" => enum!(values["ike_dh_group"], "ike_dh_group", dh_group_options),
|
|
259
|
+
"ike_lifetime" => integer!(values["ike_lifetime"], "ike_lifetime", 300..86_400),
|
|
260
|
+
"ipsec_encryption" => enum!(values["ipsec_encryption"], "ipsec_encryption", encryption_options),
|
|
261
|
+
"ipsec_authentication" => enum!(values["ipsec_authentication"], "ipsec_authentication", %w[sha256 sha1 md5]),
|
|
262
|
+
"ipsec_lifetime" => integer!(values["ipsec_lifetime"], "ipsec_lifetime", 300..86_400),
|
|
263
|
+
"add_route" => add_route,
|
|
264
|
+
"virtual_router" => identifier!(values["virtual_router"].nil? ? (@vendor == "hillstone" ? "trust-vr" : "default") : values["virtual_router"], "virtual_router"),
|
|
265
|
+
"route_metric" => integer!(values["route_metric"].nil? ? 10 : values["route_metric"], "route_metric", 1..65_535),
|
|
266
|
+
"dpd_interval" => integer!(values["dpd_interval"].nil? ? 10 : values["dpd_interval"], "dpd_interval", 1..3600),
|
|
267
|
+
"dpd_retry" => integer!(values["dpd_retry"].nil? ? (@vendor == "hillstone" ? 3 : 10) : values["dpd_retry"], "dpd_retry", 1..100)
|
|
268
|
+
}
|
|
269
|
+
if @vendor == "hillstone"
|
|
270
|
+
result["hillstone_sa_index"] = integer!(values["hillstone_sa_index"].nil? ? 35 : values["hillstone_sa_index"], "hillstone_sa_index", 1..65_535)
|
|
271
|
+
result["tunnel_zone"] = identifier!(values["tunnel_zone"].nil? ? "VPNHub" : values["tunnel_zone"], "tunnel_zone")
|
|
272
|
+
end
|
|
273
|
+
finish!(result)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def take!(fields)
|
|
277
|
+
fields.index_with { |field| @input.delete(field) }
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def finish!(result)
|
|
281
|
+
raise Error, "unexpected fields: #{@input.keys.join(", ")}" if @input.any?
|
|
282
|
+
result
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def object!(value, path)
|
|
286
|
+
Support.object(value, name: path)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def exact!(value, fields, path)
|
|
290
|
+
row = object!(value, path)
|
|
291
|
+
unexpected = row.keys - fields
|
|
292
|
+
raise Error, "unexpected #{path} fields: #{unexpected.join(", ")}" if unexpected.any?
|
|
293
|
+
fields.index_with { |field| row[field] }
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def rows!(value, path)
|
|
297
|
+
raise Error, "#{path} must contain 1 to #{MAX_ROWS} rows" unless value.is_a?(Array) && value.size.between?(1, MAX_ROWS)
|
|
298
|
+
value
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def string!(value, path)
|
|
302
|
+
raise Error, "#{path} is required" unless value.is_a?(String) && value.strip.present?
|
|
303
|
+
value.strip
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def hostname!(value, path)
|
|
307
|
+
text = string!(value, path)
|
|
308
|
+
raise Error, "#{path} is invalid" unless text.match?(/\A(?=.{1,63}\z)[a-zA-Z0-9](?:[a-zA-Z0-9.-]*[a-zA-Z0-9])?\z/)
|
|
309
|
+
text
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def identifier!(value, path)
|
|
313
|
+
text = string!(value, path)
|
|
314
|
+
raise Error, "#{path} is invalid" unless text.match?(/\A[a-zA-Z0-9][a-zA-Z0-9_.-]{0,63}\z/)
|
|
315
|
+
text
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def optional_identifier(value, path) = provided?(value) ? identifier!(value, path) : nil
|
|
319
|
+
|
|
320
|
+
def description!(value, path)
|
|
321
|
+
text = string!(value, path)
|
|
322
|
+
raise Error, "#{path} is invalid" unless text.length <= 160 && !text.match?(/[\x00-\x1f\x7f;"\\]/)
|
|
323
|
+
text
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def optional_description(value, path) = provided?(value) ? description!(value, path) : nil
|
|
327
|
+
|
|
328
|
+
def interface!(value, path)
|
|
329
|
+
text = string!(value, path)
|
|
330
|
+
raise Error, "#{path} is invalid" unless text.match?(/\A[a-zA-Z][a-zA-Z0-9._\/-]{0,63}\z/)
|
|
331
|
+
text
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def ipv4!(value, path)
|
|
335
|
+
text = string!(value, path)
|
|
336
|
+
address = IPAddr.new(text)
|
|
337
|
+
raise Error, "#{path} must be IPv4" unless address.ipv4? && !text.include?("/")
|
|
338
|
+
text
|
|
339
|
+
rescue IPAddr::InvalidAddressError
|
|
340
|
+
raise Error, "#{path} must be IPv4"
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def cidr!(value, path, network: false, host: false)
|
|
344
|
+
text = string!(value, path)
|
|
345
|
+
raise Error, "#{path} must be IPv4 CIDR" unless text.match?(/\A(?:\d{1,3}\.){3}\d{1,3}\/(?:[0-9]|[12][0-9]|3[0-2])\z/) && IPAddr.new(text).ipv4?
|
|
346
|
+
address = IPAddr.new(text)
|
|
347
|
+
raise Error, "#{path} must be a network address" if network && address.to_range.first.to_s != text.split("/").first
|
|
348
|
+
raise Error, "#{path} must be a host address" if host && [address.to_range.first.to_s, address.to_range.last.to_s].include?(text.split("/").first) && text.split("/").last.to_i < 31
|
|
349
|
+
text
|
|
350
|
+
rescue IPAddr::InvalidAddressError
|
|
351
|
+
raise Error, "#{path} must be IPv4 CIDR"
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def integer!(value, path, range)
|
|
355
|
+
number = value if value.is_a?(Integer)
|
|
356
|
+
number = Integer(value, 10) if value.is_a?(String) && value.match?(/\A[0-9]+\z/)
|
|
357
|
+
raise Error, "#{path} is outside the allowed range" unless number&.in?(range)
|
|
358
|
+
number
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def asn!(value, path) = integer!(value, path, 1..4_294_967_295)
|
|
362
|
+
|
|
363
|
+
def ospf_area!(value, path)
|
|
364
|
+
text = string!(value, path)
|
|
365
|
+
decimal = Integer(text, 10) if text.match?(/\A[0-9]{1,20}\z/)
|
|
366
|
+
dotted = text.match?(/\A(?:\d{1,3}\.){3}\d{1,3}\z/) && IPAddr.new(text).ipv4?
|
|
367
|
+
raise Error, "#{path} is invalid" unless decimal&.between?(0, 4_294_967_295) || dotted
|
|
368
|
+
decimal ? decimal.to_s : text
|
|
369
|
+
rescue IPAddr::InvalidAddressError
|
|
370
|
+
raise Error, "#{path} is invalid"
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def management_cidr!(value, path)
|
|
374
|
+
cidr = cidr!(value, path, host: true)
|
|
375
|
+
raise Error, "#{path} prefix must be between 1 and 30" unless cidr.split("/").last.to_i.between?(1, 30)
|
|
376
|
+
cidr
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def boolean!(value, path) = value.in?([ true, false ]) ? value : raise(Error, "#{path} must be boolean")
|
|
380
|
+
def enum!(value, path, options) = options.include?(value.to_s) ? value.to_s : raise(Error, "#{path} is invalid")
|
|
381
|
+
|
|
382
|
+
def vlan_value!(value, path)
|
|
383
|
+
text = string!(value, path).downcase
|
|
384
|
+
return "trunk" if text == "trunk"
|
|
385
|
+
item = /[0-9]{1,20}(?:-[0-9]{1,20})?/
|
|
386
|
+
raise Error, "#{path} is invalid" unless text.match?(/\A#{item}(?:(?: *, *| +)#{item})*\z/)
|
|
387
|
+
|
|
388
|
+
text.split(/[ ,]+/).map do |part|
|
|
389
|
+
bounds = part.split("-").map { |bound| integer!(bound, path, 1..4094) }
|
|
390
|
+
raise Error, "#{path} is invalid" if bounds.size == 2 && bounds.first > bounds.last
|
|
391
|
+
|
|
392
|
+
bounds.join("-")
|
|
393
|
+
end.join(",")
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def secret!(value, path)
|
|
397
|
+
text = string!(value, path)
|
|
398
|
+
raise Error, "#{path} is invalid" unless text.match?(/\A[!-~]{8,128}\z/) && !text.match?(/["\\;`]/)
|
|
399
|
+
text
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def string_list!(value, path)
|
|
403
|
+
raise Error, "#{path} must be an array or lines of text" unless value.is_a?(String) || (value.is_a?(Array) && value.all?(String))
|
|
404
|
+
values = value.is_a?(Array) ? value : value.lines
|
|
405
|
+
values = values.map { |item| item.to_s.strip }.reject(&:blank?).uniq
|
|
406
|
+
raise Error, "#{path} must contain 1 to #{MAX_ROWS} values" unless values.size.between?(1, MAX_ROWS)
|
|
407
|
+
values
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def policy_zone!(value, path)
|
|
411
|
+
text = string!(value, path)
|
|
412
|
+
return "any" if text.casecmp?("any")
|
|
413
|
+
identifier!(text, path)
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def policy_description!(value, path)
|
|
417
|
+
return unless provided?(value)
|
|
418
|
+
text = description!(value, path)
|
|
419
|
+
raise Error, "#{path} is invalid" if text.match?(/["\\]/)
|
|
420
|
+
text
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def policy_address_list!(value, path)
|
|
424
|
+
values = string_list!(value, path).map.with_index do |address, index|
|
|
425
|
+
policy_address!(address, "#{path}[#{index}]")
|
|
426
|
+
end.uniq
|
|
427
|
+
if values.include?("any") && values.size > 1
|
|
428
|
+
raise Error, "#{path} cannot combine any with other addresses"
|
|
429
|
+
end
|
|
430
|
+
values
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
def policy_address!(value, path)
|
|
434
|
+
text = string!(value, path)
|
|
435
|
+
return "any" if text.casecmp?("any")
|
|
436
|
+
if text.include?("-")
|
|
437
|
+
bounds = text.split(/\s*-\s*/, -1)
|
|
438
|
+
raise Error, "#{path} must be IPv4, CIDR, range, or any" unless bounds.size == 2
|
|
439
|
+
first = ipv4!(bounds.first, path)
|
|
440
|
+
last = ipv4!(bounds.last, path)
|
|
441
|
+
raise Error, "#{path} range is reversed" if IPAddr.new(first).to_i > IPAddr.new(last).to_i
|
|
442
|
+
return "#{first}-#{last}"
|
|
443
|
+
end
|
|
444
|
+
return cidr!(text, path) if text.include?("/")
|
|
445
|
+
"#{ipv4!(text, path)}/32"
|
|
446
|
+
rescue IPAddr::InvalidAddressError
|
|
447
|
+
raise Error, "#{path} must be IPv4, CIDR, range, or any"
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def policy_service_list!(value, path)
|
|
451
|
+
values = string_list!(value, path).map.with_index do |service, index|
|
|
452
|
+
policy_service!(service, "#{path}[#{index}]")
|
|
453
|
+
end.uniq
|
|
454
|
+
if values.include?("any") && values.size > 1
|
|
455
|
+
raise Error, "#{path} cannot combine any with other ports"
|
|
456
|
+
end
|
|
457
|
+
values
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def policy_service!(value, path)
|
|
461
|
+
text = string!(value, path).downcase
|
|
462
|
+
return "any" if text == "any"
|
|
463
|
+
unless text.match?(/\A[0-9]{1,20}(?:\s*-\s*[0-9]{1,20})?\z/)
|
|
464
|
+
raise Error, "#{path} must be a port, port range, or any"
|
|
465
|
+
end
|
|
466
|
+
bounds = text.split(/\s*-\s*/, -1).map { |port| Integer(port, 10) }
|
|
467
|
+
unless bounds.size.between?(1, 2) && bounds.all? { |port| port&.between?(1, 65_535) }
|
|
468
|
+
raise Error, "#{path} must be a port, port range, or any"
|
|
469
|
+
end
|
|
470
|
+
raise Error, "#{path} range is reversed" if bounds.size == 2 && bounds.first > bounds.last
|
|
471
|
+
bounds.uniq.join("-")
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def system_configuration!(overrides, access_auth_enabled: false)
|
|
475
|
+
config = SystemConfiguration.resolve(object!(overrides, "system"), defaults: @system_config)
|
|
476
|
+
unless config["local_users"].is_a?(Array) && config["local_users"].size.between?(1, 16)
|
|
477
|
+
raise Error, "system.local_users must contain 1 to 16 rows"
|
|
478
|
+
end
|
|
479
|
+
result = {
|
|
480
|
+
"domain_name" => domain!(config["domain_name"], "system.domain_name"),
|
|
481
|
+
"timezone" => enum!(config["timezone"], "system.timezone", %w[Beijing]),
|
|
482
|
+
"management_vlan" => integer!(config["management_vlan"], "system.management_vlan", 1..4094),
|
|
483
|
+
"ntp_servers" => ipv4_list!(config["ntp_servers"], "system.ntp_servers"),
|
|
484
|
+
"dns_servers" => ipv4_list!(config["dns_servers"], "system.dns_servers"),
|
|
485
|
+
"syslog_servers" => ipv4_list!(config["syslog_servers"], "system.syslog_servers"),
|
|
486
|
+
"snmp_community" => secret!(config["snmp_community"], "system.snmp_community"),
|
|
487
|
+
"tacacs_servers" => ipv4_list!(config["tacacs_servers"], "system.tacacs_servers"),
|
|
488
|
+
"tacacs_secret" => secret!(config["tacacs_secret"], "system.tacacs_secret"),
|
|
489
|
+
"ssh_acl_cidrs" => string_list!(config["ssh_acl_cidrs"], "system.ssh_acl_cidrs").map { |cidr| cidr!(cidr, "system.ssh_acl_cidrs") },
|
|
490
|
+
"local_users" => rows!(config["local_users"], "system.local_users").map.with_index { |user, index| system_user!(user, index) },
|
|
491
|
+
"base_security_enabled" => boolean!(config["base_security_enabled"], "system.base_security_enabled"),
|
|
492
|
+
"disable_telnet" => boolean!(config["disable_telnet"], "system.disable_telnet"),
|
|
493
|
+
"disable_http" => boolean!(config["disable_http"], "system.disable_http"),
|
|
494
|
+
"enable_bpdu_protection" => boolean!(config["enable_bpdu_protection"], "system.enable_bpdu_protection"),
|
|
495
|
+
"password_min_length" => integer!(config["password_min_length"], "system.password_min_length", 8..64),
|
|
496
|
+
"vty_timeout_minutes" => integer!(config["vty_timeout_minutes"], "system.vty_timeout_minutes", 1..120)
|
|
497
|
+
}
|
|
498
|
+
if access_auth_enabled
|
|
499
|
+
result["radius_servers"] = ipv4_list!(config["radius_servers"], "system.radius_servers")
|
|
500
|
+
result["radius_secret"] = secret!(config["radius_secret"], "system.radius_secret")
|
|
501
|
+
end
|
|
502
|
+
raise Error, "system local usernames must be unique" unless result["local_users"].map { |user| user["username"].downcase }.uniq.size == result["local_users"].size
|
|
503
|
+
result
|
|
504
|
+
rescue KeyError => exception
|
|
505
|
+
raise Error, exception.message
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
def system_user!(value, index)
|
|
509
|
+
user = exact!(value, %w[username password role], "system.local_users[#{index}]")
|
|
510
|
+
username = string!(user["username"], "system.local_users[#{index}].username")
|
|
511
|
+
raise Error, "system.local_users[#{index}].username is invalid" unless username.match?(/\A[a-zA-Z0-9][a-zA-Z0-9_-]{2,31}\z/)
|
|
512
|
+
{ "username" => username,
|
|
513
|
+
"password" => secret!(user["password"], "system.local_users[#{index}].password"),
|
|
514
|
+
"role" => enum!(user["role"], "system.local_users[#{index}].role", %w[admin operator read_only]) }
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
def ipv4_list!(value, path) = string_list!(value, path).map { |address| ipv4!(address, path) }
|
|
518
|
+
|
|
519
|
+
def domain!(value, path)
|
|
520
|
+
text = string!(value, path)
|
|
521
|
+
raise Error, "#{path} is invalid" unless text.match?(/\A(?=.{1,253}\z)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\z/i)
|
|
522
|
+
text
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
def last_usable(cidr)
|
|
526
|
+
range = IPAddr.new(cidr).to_range
|
|
527
|
+
IPAddr.new(range.last.to_i - 1, Socket::AF_INET).to_s
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
def automatic_local_description(remote, remote_iface, aggregated)
|
|
531
|
+
[ "TO", remote.upcase, (remote_iface.upcase unless aggregated) ].compact.join("_")
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
def automatic_group_name(flag, app_port)
|
|
535
|
+
identifier!("#{flag}_#{app_port}".first(64), "services.group_name")
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def assign_automatic_aggregate_ids!(allocations, mode)
|
|
539
|
+
used = allocations.filter_map { |row| row["aggregate_id"] }.uniq
|
|
540
|
+
allocations.each_with_index do |row, index|
|
|
541
|
+
next unless row["port_channel"] && row["aggregate_id"].nil?
|
|
542
|
+
|
|
543
|
+
group = if mode == "vpc"
|
|
544
|
+
[ row ]
|
|
545
|
+
else
|
|
546
|
+
remote_rows = allocations.select { |item| item["port_channel"] && item["remote"].casecmp?(row["remote"]) }
|
|
547
|
+
position = remote_rows.index(row)
|
|
548
|
+
remote_rows.each_slice(2).to_a.fetch(position / 2)
|
|
549
|
+
end
|
|
550
|
+
aggregate_id = group.filter_map { |item| item["aggregate_id"] }.first
|
|
551
|
+
aggregate_id ||= automatic_aggregate_id(group.first.fetch("local_iface"), used, index)
|
|
552
|
+
group.each { |item| item["aggregate_id"] ||= aggregate_id }
|
|
553
|
+
used << aggregate_id
|
|
554
|
+
end
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
def automatic_aggregate_id(local_iface, used, index)
|
|
558
|
+
suffix = local_iface.scan(/\d+/).last.to_i
|
|
559
|
+
candidate = suffix.positive? ? suffix + 100 : index + 101
|
|
560
|
+
return candidate if candidate.between?(1, 4096) && !used.include?(candidate)
|
|
561
|
+
|
|
562
|
+
(101..4096).find { |value| !used.include?(value) }
|
|
563
|
+
end
|
|
564
|
+
end
|
|
565
|
+
end
|
|
566
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Adapted from net-connector; copyright (c) 2026 net-connector contributors (MIT).
|
|
4
|
+
|
|
5
|
+
module Netstack
|
|
6
|
+
module Connector
|
|
7
|
+
# 密码或密钥登录与显式特权认证共用有界对话流程。
|
|
8
|
+
class Authentication
|
|
9
|
+
CONNECTION_FAILURES = {
|
|
10
|
+
/REMOTE HOST IDEN|IDENTIFICATION CHANGED/i => [ConnectionError, :host_key_changed],
|
|
11
|
+
/Host key verification failed/i => [ConnectionError, :host_key_untrusted],
|
|
12
|
+
/RSA modulus too small/i => [ConnectionError, :rsa_too_small],
|
|
13
|
+
/Selected cipher type <unknown> not supported by server/i => [ConnectionError, :unsupported_cipher],
|
|
14
|
+
/Connection refused/i => [ConnectionError, :connection_refused],
|
|
15
|
+
/No route to host/i => [ConnectionError, :no_route],
|
|
16
|
+
/Connection reset/i => [ConnectionError, :connection_reset],
|
|
17
|
+
/(?:Operation|Connection) timed out/i => [ConnectionError, :connection_timeout]
|
|
18
|
+
}.freeze
|
|
19
|
+
SSH_CONFIRMATION = %r{\(yes/no(?:/\[fingerprint\])?\)\?\s*\z}i
|
|
20
|
+
|
|
21
|
+
# 保存会话配置,并合并传输失败和设备认证失败模式。
|
|
22
|
+
def initialize(session, dialogue)
|
|
23
|
+
@session = session
|
|
24
|
+
@dialogue = dialogue
|
|
25
|
+
@config = session.configuration
|
|
26
|
+
@failures = CONNECTION_FAILURES.merge(
|
|
27
|
+
dialogue.authentication_errors.to_h { |pattern| [pattern, [AuthenticationError, :authentication_failed]] }
|
|
28
|
+
).freeze
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# 在登录提示或连接失败模式出现前读取登录响应。
|
|
32
|
+
def login
|
|
33
|
+
@session.reader.read(
|
|
34
|
+
prompt: @dialogue.login_prompt, interactions: login_interactions, failures: @failures,
|
|
35
|
+
timeout: @config.login_timeout, phase: :login
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# 发送特权命令,并在截止时间内完成凭据交互。
|
|
40
|
+
def enable(command, prompt)
|
|
41
|
+
deadline = Expect.monotonic + @config.login_timeout
|
|
42
|
+
@session.write("#{command}\n", deadline: deadline, phase: :enable)
|
|
43
|
+
secret = @config.enable_password || @config.password
|
|
44
|
+
@session.reader.read(prompt: prompt, interactions: credential_interactions(secret), failures: @failures,
|
|
45
|
+
deadline: deadline, phase: :enable)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# 为用户名和密码创建一次性敏感交互规则。
|
|
51
|
+
def credential_interactions(secret)
|
|
52
|
+
[
|
|
53
|
+
Interaction.new(@dialogue.username_prompt, ->(_prompt) { input_line(@config.username) },
|
|
54
|
+
sensitive: true, limit: 1, capture: false),
|
|
55
|
+
Interaction.new(@dialogue.password_prompt, ->(_prompt) { input_line(secret) },
|
|
56
|
+
sensitive: true, limit: 1, capture: false)
|
|
57
|
+
]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# 组装主机密钥确认、厂商挑战和凭据交互。
|
|
61
|
+
def login_interactions
|
|
62
|
+
host_key = Interaction.new(SSH_CONFIRMATION, lambda { |_prompt|
|
|
63
|
+
if @config.host_key_policy == :strict
|
|
64
|
+
raise @session.build_error(ConnectionError, "host key confirmation rejected",
|
|
65
|
+
phase: :login, code: :host_key_untrusted), cause: nil
|
|
66
|
+
end
|
|
67
|
+
"yes\n"
|
|
68
|
+
}, limit: 1)
|
|
69
|
+
[host_key, *@dialogue.login_interactions, *@config.challenges.map(&:challenge),
|
|
70
|
+
*credential_interactions(@config.password)]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# 将可选凭据编码为带换行的设备输入。
|
|
74
|
+
def input_line(secret) = secret && "#{secret}\n"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|