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,168 @@
|
|
|
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/payload"
|
|
9
|
+
|
|
10
|
+
module Netstack
|
|
11
|
+
module Configuration
|
|
12
|
+
class Payload
|
|
13
|
+
def self.call(scenario:, input:, system_config: {}, now: Time.now.utc)
|
|
14
|
+
data = Support.object(input).deep_dup
|
|
15
|
+
data["system"] = system_config.deep_stringify_keys if scenario.to_s == "bootstrap"
|
|
16
|
+
case scenario.to_s
|
|
17
|
+
when "bootstrap" then bootstrap(data)
|
|
18
|
+
when "h3c-irf" then irf(data)
|
|
19
|
+
when "distributed-gateway" then distributed_gateway(data)
|
|
20
|
+
when "server-port" then server_port(data)
|
|
21
|
+
when "hillstone-policy" then hillstone_policy(data, now: now)
|
|
22
|
+
when "load-balancer" then load_balancer(data)
|
|
23
|
+
when "ipsec-vpn" then ipsec(data)
|
|
24
|
+
end
|
|
25
|
+
data
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class << self
|
|
29
|
+
include Bootstrap::Payload
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def irf(data)
|
|
34
|
+
range = IPAddr.new(data.fetch("mad_subnet")).to_range
|
|
35
|
+
data.merge!("mad_mask" => mask(data.fetch("mad_subnet")), "mad_member1_ip" => ipv4(range.first.to_i + 1), "mad_member2_ip" => ipv4(range.first.to_i + 2))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def server_port(data)
|
|
39
|
+
data.fetch("allocations").each { |row| decorate_switchport(row) }
|
|
40
|
+
data["aggregates"] = data.fetch("allocations").select { |row| row["port_channel"] }.group_by { |row| row["aggregate_id"] }.map { |id, rows| rows.first.merge("aggregate_id" => id) }
|
|
41
|
+
data["aggregates"].each { |row| row["vpc"] = data.fetch("stack_mode") == "vpc" }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def distributed_gateway(data)
|
|
45
|
+
data.fetch("gateways").each { |row| row["gateway_cidr"] = "#{row.fetch("gateway")}/#{row.fetch("subnet").split("/").last}" }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def hillstone_policy(data, now:)
|
|
49
|
+
policies = data.fetch("policies")
|
|
50
|
+
data["address_objects"] = []
|
|
51
|
+
service_index = {}
|
|
52
|
+
generated_date = now.strftime("%Y%m%d")
|
|
53
|
+
|
|
54
|
+
policies.each_with_index do |policy, index|
|
|
55
|
+
sequence = index + 1
|
|
56
|
+
policy["object_key"] = format("POLICY_%s_%03d", generated_date, sequence)
|
|
57
|
+
policy["description"] = policy["description"].presence || format("%s-%03d", generated_date, sequence)
|
|
58
|
+
policy["has_description"] = true
|
|
59
|
+
decorate_policy_addresses(data, policy, "src")
|
|
60
|
+
decorate_policy_addresses(data, policy, "dst")
|
|
61
|
+
|
|
62
|
+
policy["service_any"] = policy.fetch("services") == [ "any" ]
|
|
63
|
+
policy["service_names"] = []
|
|
64
|
+
policy["service_group"] = nil
|
|
65
|
+
next if policy["service_any"]
|
|
66
|
+
|
|
67
|
+
policy["service_names"] = policy.fetch("services").map do |ports|
|
|
68
|
+
name = hillstone_object_name(policy.fetch("protocol"), ports)
|
|
69
|
+
service_index[name] ||= hillstone_service(name, policy.fetch("protocol"), ports)
|
|
70
|
+
name
|
|
71
|
+
end
|
|
72
|
+
if policy["service_names"].size > 3
|
|
73
|
+
policy["service_group"] = hillstone_object_name(policy.fetch("object_key"), "SVC")
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
data["service_objects"] = service_index.values
|
|
78
|
+
data["service_groups"] = policies.filter_map do |policy|
|
|
79
|
+
next unless policy["service_group"]
|
|
80
|
+
{ "name" => policy.fetch("service_group"), "services" => policy.fetch("service_names") }
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def decorate_policy_addresses(data, policy, side)
|
|
85
|
+
addresses = policy.fetch("#{side}_addresses")
|
|
86
|
+
policy["#{side}_any"] = addresses == [ "any" ]
|
|
87
|
+
policy["#{side}_object"] = nil
|
|
88
|
+
policy["#{side}_entries"] = []
|
|
89
|
+
return if policy["#{side}_any"]
|
|
90
|
+
|
|
91
|
+
entries = addresses.map { |address| hillstone_address_entry(address) }
|
|
92
|
+
if entries.size > 3
|
|
93
|
+
object_name = hillstone_object_name(policy.fetch("object_key"), side.upcase)
|
|
94
|
+
policy["#{side}_object"] = object_name
|
|
95
|
+
data.fetch("address_objects") << { "name" => object_name, "entries" => entries }
|
|
96
|
+
else
|
|
97
|
+
policy["#{side}_entries"] = entries
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def hillstone_address_entry(address)
|
|
102
|
+
return { "range" => false, "value" => address } unless address.include?("-")
|
|
103
|
+
first, last = address.split("-", 2)
|
|
104
|
+
{ "range" => true, "first" => first, "last" => last }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def hillstone_service(name, protocol, ports)
|
|
108
|
+
bounds = ports.split("-", 2)
|
|
109
|
+
{ "name" => name, "protocol" => protocol, "first_port" => bounds.first,
|
|
110
|
+
"last_port" => bounds.last || bounds.first, "port_range" => bounds.size == 2 }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def hillstone_object_name(*parts)
|
|
114
|
+
parts.join("_").upcase
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def load_balancer(data)
|
|
118
|
+
services = data.fetch("services")
|
|
119
|
+
data["real_servers"] = services.flat_map { |row| row.fetch("real_ips") }.uniq.map { |address| { "address" => address } }
|
|
120
|
+
data["groups"] = services.group_by { |row| row.fetch("group_name").downcase }.values.map do |rows|
|
|
121
|
+
rows.first.slice("group_name", "real_ips", "health_check")
|
|
122
|
+
end
|
|
123
|
+
services.each { |row| row["client_ip_persistence"] = row.fetch("client_ip_persistence", false) }
|
|
124
|
+
data["virtuals"] = services.group_by { |row| [ row.fetch("flag"), row.fetch("virtual_ip") ] }.map do |(flag, virtual_ip), rows|
|
|
125
|
+
{ "virtual_ip" => virtual_ip, "virtual_name" => "VIP_#{flag}_#{virtual_ip}", "services" => rows }
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def ipsec(data)
|
|
130
|
+
data["ikev1"] = data.fetch("ike_version") == "ikev1"
|
|
131
|
+
data["ikev2"] = data.fetch("ike_version") == "ikev2"
|
|
132
|
+
data["local_ip"] = data["local_address"]&.split("/")&.first
|
|
133
|
+
encryption = { "aes-256-cbc" => "aes-256", "3des" => "3des" }
|
|
134
|
+
authentication = { "sha256" => "sha256", "sha1" => "sha", "md5" => "md5" }
|
|
135
|
+
data["hillstone_ike_encryption"] = encryption[data.fetch("ike_encryption")]
|
|
136
|
+
data["hillstone_ipsec_encryption"] = encryption[data.fetch("ipsec_encryption")]
|
|
137
|
+
data["hillstone_ikev2_encryption"] = data["hillstone_ike_encryption"]&.delete("-")
|
|
138
|
+
data["hillstone_ipsec_v2_encryption"] = data["hillstone_ipsec_encryption"]&.delete("-")
|
|
139
|
+
data["hillstone_ike_hash"] = authentication.fetch(data.fetch("ike_authentication"))
|
|
140
|
+
data["hillstone_ipsec_hash"] = authentication.fetch(data.fetch("ipsec_authentication"))
|
|
141
|
+
data["hillstone_ike_group"] = data.fetch("ike_dh_group").delete_prefix("group")
|
|
142
|
+
data["proxy_ids"] = data.fetch("local_subnets").product(data.fetch("remote_subnets")).map.with_index { |(local, remote), index| { "name" => "proxy-#{index + 1}", "local" => local, "remote" => remote } }
|
|
143
|
+
data["routes"] = data.fetch("remote_subnets").map.with_index { |subnet, index| { "name" => "#{data.fetch("site_name")}-#{index + 1}", "subnet" => subnet } }
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def mask(cidr) = IPAddr.new("255.255.255.255").mask(cidr.split("/").last.to_i).to_s
|
|
147
|
+
def ipv4(number) = IPAddr.new(number, Socket::AF_INET).to_s
|
|
148
|
+
|
|
149
|
+
def entries(addresses)
|
|
150
|
+
addresses.map.with_index { |address, index| { "address" => address, "role" => index.zero? ? "primary" : "secondary", "name" => "SERVER#{index + 1}" } }
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def wildcard(cidr)
|
|
154
|
+
network = IPAddr.new(cidr)
|
|
155
|
+
mask_value = IPAddr.new("255.255.255.255").mask(cidr.split("/").last.to_i)
|
|
156
|
+
"#{network.to_range.first} #{ipv4(mask_value.to_i ^ 0xffffffff)}"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def decorate_switchport(row)
|
|
160
|
+
row["trunk_all"] = row["vlan"] == "trunk"
|
|
161
|
+
row["trunk_list"] = !row["trunk_all"] && row["vlan"].match?(/[,-]/)
|
|
162
|
+
row["access"] = !row["trunk_all"] && !row["trunk_list"]
|
|
163
|
+
row["vlan_space"] = row["vlan"].tr(",", " ").gsub("-", " to ")
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "active_support/core_ext/object/blank"
|
|
5
|
+
|
|
6
|
+
module Netstack
|
|
7
|
+
module Configuration
|
|
8
|
+
# Source bytes and line endings are immutable; consumption belongs to this reader only.
|
|
9
|
+
class Reader
|
|
10
|
+
attr_reader :content, :lines, :vendor, :device_id, :device_name, :created_at, :checksum, :position
|
|
11
|
+
|
|
12
|
+
def initialize(content, vendor: nil, device_id: nil, device_name: nil, created_at: Time.now.utc)
|
|
13
|
+
raise ArgumentError, "Configuration content must be a string" unless content.is_a?(String)
|
|
14
|
+
|
|
15
|
+
@content = content.dup
|
|
16
|
+
unless [Encoding::UTF_8, Encoding::BINARY, Encoding::US_ASCII].include?(@content.encoding)
|
|
17
|
+
raise EncodingError, "Configuration must contain UTF-8 bytes"
|
|
18
|
+
end
|
|
19
|
+
@content.force_encoding(Encoding::UTF_8)
|
|
20
|
+
raise EncodingError, "Configuration is not valid UTF-8" unless @content.valid_encoding?
|
|
21
|
+
raise ArgumentError, "Configuration contains a NUL byte" if @content.include?("\0")
|
|
22
|
+
|
|
23
|
+
@content.freeze
|
|
24
|
+
@vendor = vendor.nil? ? nil : identifier(vendor, "Vendor").downcase.tr(" -", "__").freeze
|
|
25
|
+
@device_id = device_id.nil? ? nil : identifier(device_id, "Device ID")
|
|
26
|
+
@device_name = device_name.nil? ? nil : identifier(device_name, "Device name")
|
|
27
|
+
raise ArgumentError, "Creation time must be a Time" unless created_at.is_a?(Time)
|
|
28
|
+
|
|
29
|
+
@created_at = created_at.dup.freeze
|
|
30
|
+
@lines = @content.scan(/[^\r\n]*(?:\r\n|\r|\n)|[^\r\n]+\z/).map(&:freeze).freeze
|
|
31
|
+
@checksum = Digest::SHA256.hexdigest(@content).freeze
|
|
32
|
+
reset!
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def size = lines.size
|
|
36
|
+
def inspect = "#<#{self.class.name} bytes=#{content.bytesize} lines=#{size} checksum=#{checksum}>"
|
|
37
|
+
def parsed_flags = @parsed.dup.freeze
|
|
38
|
+
def parsed?(index) = valid_index?(index) ? @parsed[index] : nil
|
|
39
|
+
|
|
40
|
+
def mark(index, parsed: true)
|
|
41
|
+
raise IndexError, "Line index is outside the document" unless valid_index?(index)
|
|
42
|
+
raise ArgumentError, "Parsed flag must be true or false" unless parsed == true || parsed == false
|
|
43
|
+
|
|
44
|
+
@parsed[index] = parsed
|
|
45
|
+
self
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def rewind
|
|
49
|
+
@position = 0
|
|
50
|
+
self
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def reset!
|
|
54
|
+
@parsed = Array.new(size, false)
|
|
55
|
+
rewind
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def read
|
|
59
|
+
return nil if position >= size
|
|
60
|
+
|
|
61
|
+
line = lines[position]
|
|
62
|
+
@position += 1
|
|
63
|
+
line
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def read_pending
|
|
67
|
+
while position < size
|
|
68
|
+
index = position
|
|
69
|
+
line = read
|
|
70
|
+
next if @parsed[index]
|
|
71
|
+
|
|
72
|
+
@parsed[index] = true
|
|
73
|
+
line = line.chomp
|
|
74
|
+
next if line.blank?
|
|
75
|
+
|
|
76
|
+
return line.freeze
|
|
77
|
+
end
|
|
78
|
+
nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Cursor commands return whether they changed state, rather than answering a predicate.
|
|
82
|
+
# rubocop:disable Naming/PredicateMethod
|
|
83
|
+
def step_back
|
|
84
|
+
return false if position.zero?
|
|
85
|
+
|
|
86
|
+
@position -= 1
|
|
87
|
+
true
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def unread
|
|
91
|
+
return false unless step_back
|
|
92
|
+
|
|
93
|
+
@parsed[position] = false
|
|
94
|
+
true
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def defer
|
|
98
|
+
return false if position.zero?
|
|
99
|
+
|
|
100
|
+
@parsed[position - 1] = false
|
|
101
|
+
true
|
|
102
|
+
end
|
|
103
|
+
# rubocop:enable Naming/PredicateMethod
|
|
104
|
+
|
|
105
|
+
def unparsed_lines = lines.each_index.filter_map { |index| lines[index] unless @parsed[index] }.freeze
|
|
106
|
+
def unparsed_content = unparsed_lines.join.freeze
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
def valid_index?(index) = index.is_a?(Integer) && index >= 0 && index < size
|
|
111
|
+
|
|
112
|
+
def identifier(value, label)
|
|
113
|
+
text = case value
|
|
114
|
+
when String then value
|
|
115
|
+
when Symbol then value.to_s
|
|
116
|
+
when Integer then value.to_s if value >= 0
|
|
117
|
+
end
|
|
118
|
+
raise ArgumentError, "#{label} must be nonempty text or a nonnegative integer" unless text && !text.strip.empty?
|
|
119
|
+
|
|
120
|
+
text.dup.freeze
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/core_ext/object/blank"
|
|
4
|
+
require "active_support/core_ext/object/deep_dup"
|
|
5
|
+
require "active_support/core_ext/object/inclusion"
|
|
6
|
+
require "active_support/core_ext/hash/keys"
|
|
7
|
+
require "active_support/core_ext/enumerable"
|
|
8
|
+
require "active_support/core_ext/string/access"
|
|
9
|
+
|
|
10
|
+
module Netstack
|
|
11
|
+
module Configuration
|
|
12
|
+
# Shared framework extensions, without loading Rails or Active Record.
|
|
13
|
+
module Support
|
|
14
|
+
def self.object(value, name: "input")
|
|
15
|
+
raise InputError, "#{name} must be an object" unless value.is_a?(Hash)
|
|
16
|
+
duplicate = value.keys.map(&:to_s).tally.values.any? { |count| count > 1 }
|
|
17
|
+
raise InputError, "#{name} contains duplicate field names" if duplicate
|
|
18
|
+
|
|
19
|
+
value.to_h do |key, item|
|
|
20
|
+
normalized = case item
|
|
21
|
+
when Hash then object(item, name: name)
|
|
22
|
+
when Array then item.map { |element| element.is_a?(Hash) ? object(element, name: name) : Values.copy(element) }
|
|
23
|
+
else Values.copy(item)
|
|
24
|
+
end
|
|
25
|
+
[key.to_s, normalized]
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "support"
|
|
4
|
+
|
|
5
|
+
module Netstack
|
|
6
|
+
module Configuration
|
|
7
|
+
class SystemConfiguration
|
|
8
|
+
REQUIRED_FIELDS = %w[domain_name timezone management_vlan ntp_servers dns_servers syslog_servers
|
|
9
|
+
snmp_community tacacs_servers tacacs_secret ssh_acl_cidrs local_users base_security_enabled
|
|
10
|
+
disable_telnet disable_http enable_bpdu_protection password_min_length vty_timeout_minutes].freeze
|
|
11
|
+
OPTIONAL_FIELDS = %w[radius_servers radius_secret].freeze
|
|
12
|
+
FIELDS = (REQUIRED_FIELDS + OPTIONAL_FIELDS).freeze
|
|
13
|
+
SECRET_FIELDS = %w[snmp_community tacacs_secret radius_secret].freeze
|
|
14
|
+
|
|
15
|
+
def self.resolve(overrides, defaults: {})
|
|
16
|
+
current = Support.object(defaults, name: "system defaults")
|
|
17
|
+
incoming = Support.object(overrides, name: "system")
|
|
18
|
+
unexpected = (current.keys | incoming.keys) - FIELDS
|
|
19
|
+
raise InputError, "unexpected system fields: #{unexpected.join(", ")}" if unexpected.any?
|
|
20
|
+
|
|
21
|
+
SECRET_FIELDS.each do |field|
|
|
22
|
+
[current, incoming].each do |values|
|
|
23
|
+
value = values[field]
|
|
24
|
+
raise InputError, "system #{field} must be a string" unless value.nil? || value.is_a?(String)
|
|
25
|
+
end
|
|
26
|
+
incoming.delete(field) if omitted_secret?(incoming[field])
|
|
27
|
+
end
|
|
28
|
+
if incoming.key?("local_users")
|
|
29
|
+
raise InputError, "system local users must be an array" unless incoming["local_users"].is_a?(Array)
|
|
30
|
+
|
|
31
|
+
saved = Array(current["local_users"]).to_h { |user| [user.fetch("username"), user] }
|
|
32
|
+
incoming["local_users"] = incoming["local_users"].map do |user|
|
|
33
|
+
normalized = Support.object(user, name: "system local user")
|
|
34
|
+
normalized["password"] = saved.dig(normalized["username"], "password") if omitted_secret?(normalized["password"])
|
|
35
|
+
normalized
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
result = current.merge(incoming)
|
|
39
|
+
missing = REQUIRED_FIELDS.select { |field| result[field].nil? || (result[field].respond_to?(:empty?) && result[field].empty?) }
|
|
40
|
+
raise InputError, "system configuration is incomplete: #{missing.join(", ")}" if missing.any?
|
|
41
|
+
|
|
42
|
+
result
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.frontend_defaults(config)
|
|
46
|
+
result = Support.object(config, name: "system")
|
|
47
|
+
SECRET_FIELDS.each { |field| result[field] = "" }
|
|
48
|
+
result["local_users"] = Array(result["local_users"]).map { |user| user.merge("password" => "") }
|
|
49
|
+
result
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.omitted_secret?(value) = value.nil? || (value.is_a?(String) && value.blank?)
|
|
53
|
+
private_class_method :omitted_secret?
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,174 @@
|
|
|
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
|
+
# A deliberately small, safe ERB dialect: interpolation, collection loops and
|
|
9
|
+
# boolean conditionals only. It never evaluates Ruby code.
|
|
10
|
+
class TemplateManager
|
|
11
|
+
Error = Class.new(Netstack::InputError)
|
|
12
|
+
TOKEN = /(<%=?[\s\S]*?%>)/
|
|
13
|
+
PATH = /\A[a-z_][a-z0-9_]*(?:\.[a-z_][a-z0-9_]*)*\z/i
|
|
14
|
+
EACH = /\A([a-z_][a-z0-9_.]*)\.each\s+do\s+\|([a-z_][a-z0-9_]*)\|\z/i
|
|
15
|
+
IF = /\Aif\s+([a-z_][a-z0-9_.]*)\z/i
|
|
16
|
+
MAX_SOURCE_BYTES = 1_048_576
|
|
17
|
+
MAX_OUTPUT_BYTES = 16_777_216
|
|
18
|
+
MAX_DEPTH = 32
|
|
19
|
+
MAX_RENDER_STEPS = 1_000_000
|
|
20
|
+
|
|
21
|
+
attr_reader :template_name, :data
|
|
22
|
+
|
|
23
|
+
def initialize(template_name, data, source: nil, store: TemplateStore.new)
|
|
24
|
+
name = template_name.to_s
|
|
25
|
+
@template_name = name.end_with?(".erb") ? name : "#{name}.erb"
|
|
26
|
+
@data = Values.copy(Support.object(data, name: "template data"))
|
|
27
|
+
@source = source
|
|
28
|
+
@store = store
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def generate
|
|
32
|
+
nodes = self.class.parse(@source || @store.fetch(@template_name))
|
|
33
|
+
output = +""
|
|
34
|
+
render_nodes(nodes, @data, output, [MAX_RENDER_STEPS])
|
|
35
|
+
output
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def inspect = "#<#{self.class.name} template=#{template_name.inspect}>"
|
|
39
|
+
|
|
40
|
+
def self.validate!(source, variables: nil)
|
|
41
|
+
nodes = parse(source)
|
|
42
|
+
validate_variables!(nodes, variables.map(&:to_s), []) if variables
|
|
43
|
+
nodes
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.validate_variables!(nodes, variables, locals)
|
|
47
|
+
nodes.each do |type, *parts|
|
|
48
|
+
case type
|
|
49
|
+
when :value
|
|
50
|
+
validate_root!(parts[0], variables, locals)
|
|
51
|
+
when :each
|
|
52
|
+
collection, variable, children = parts
|
|
53
|
+
validate_root!(collection, variables, locals)
|
|
54
|
+
validate_variables!(children, variables, locals + [ variable ])
|
|
55
|
+
when :if
|
|
56
|
+
expression, truthy, falsy = parts
|
|
57
|
+
validate_root!(expression, variables, locals)
|
|
58
|
+
validate_variables!(truthy, variables, locals)
|
|
59
|
+
validate_variables!(falsy, variables, locals)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.validate_root!(path, variables, locals)
|
|
65
|
+
root = path.split(".").first
|
|
66
|
+
raise Error, "unknown template variable: #{root}" unless variables.include?(root) || locals.include?(root)
|
|
67
|
+
end
|
|
68
|
+
private_class_method :validate_variables!, :validate_root!
|
|
69
|
+
|
|
70
|
+
def self.parse(source)
|
|
71
|
+
raise Error, "template must be UTF-8 text" unless source.is_a?(String) && source.valid_encoding?
|
|
72
|
+
raise Error, "template exceeds the byte limit" if source.bytesize > MAX_SOURCE_BYTES
|
|
73
|
+
|
|
74
|
+
root = []
|
|
75
|
+
frames = [ { type: :root, nodes: root } ]
|
|
76
|
+
source.split(TOKEN).reject(&:empty?).each do |token|
|
|
77
|
+
raise Error, "unclosed template delimiter" if token.start_with?("<%") && !token.end_with?("%>")
|
|
78
|
+
|
|
79
|
+
if token.start_with?("<%=")
|
|
80
|
+
expression = token.delete_prefix("<%=").delete_suffix("%>").strip
|
|
81
|
+
validate_path!(expression)
|
|
82
|
+
frames.last.fetch(:nodes) << [ :value, expression ]
|
|
83
|
+
elsif token.start_with?("<%")
|
|
84
|
+
parse_directive!(token.delete_prefix("<%").delete_suffix("%>").strip, frames)
|
|
85
|
+
else
|
|
86
|
+
raise Error, "invalid template delimiter" if token.include?("<%") || token.include?("%>")
|
|
87
|
+
|
|
88
|
+
frames.last.fetch(:nodes) << [ :text, token ]
|
|
89
|
+
end
|
|
90
|
+
raise Error, "template nesting exceeds the limit" if frames.size > MAX_DEPTH
|
|
91
|
+
end
|
|
92
|
+
raise Error, "template block is not closed" unless frames.one?
|
|
93
|
+
root
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def self.parse_directive!(directive, frames)
|
|
97
|
+
if (match = EACH.match(directive))
|
|
98
|
+
node = [ :each, match[1], match[2], [] ]
|
|
99
|
+
frames.last.fetch(:nodes) << node
|
|
100
|
+
frames << { type: :each, nodes: node[3] }
|
|
101
|
+
elsif (match = IF.match(directive))
|
|
102
|
+
node = [ :if, match[1], [], [] ]
|
|
103
|
+
frames.last.fetch(:nodes) << node
|
|
104
|
+
frames << { type: :if, nodes: node[2], node: node }
|
|
105
|
+
elsif directive == "else"
|
|
106
|
+
frame = frames.last
|
|
107
|
+
raise Error, "else must belong to an if block" unless frame[:type] == :if && !frame[:else]
|
|
108
|
+
frame[:else] = true
|
|
109
|
+
frame[:nodes] = frame.fetch(:node)[3]
|
|
110
|
+
elsif directive == "end"
|
|
111
|
+
raise Error, "unexpected end" if frames.one?
|
|
112
|
+
frames.pop
|
|
113
|
+
else
|
|
114
|
+
raise Error, "unsupported template expression"
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def self.validate_path!(path)
|
|
119
|
+
raise Error, "invalid template variable" unless PATH.match?(path)
|
|
120
|
+
end
|
|
121
|
+
private_class_method :parse_directive!, :validate_path!
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
def render_nodes(nodes, context, output, budget)
|
|
126
|
+
nodes.each do |type, *parts|
|
|
127
|
+
consume_work!(budget)
|
|
128
|
+
case type
|
|
129
|
+
when :text then append!(output, parts[0])
|
|
130
|
+
when :value
|
|
131
|
+
value = resolve(parts[0], context)
|
|
132
|
+
unless value.nil? || value.is_a?(String) || value.is_a?(Numeric) || [true, false].include?(value)
|
|
133
|
+
raise Error, "interpolation must resolve to a scalar"
|
|
134
|
+
end
|
|
135
|
+
append!(output, value.to_s)
|
|
136
|
+
when :each
|
|
137
|
+
collection, variable, children = parts
|
|
138
|
+
items = resolve(collection, context)
|
|
139
|
+
raise Error, "template collection must be an array" unless items.is_a?(Array)
|
|
140
|
+
|
|
141
|
+
items.each do |item|
|
|
142
|
+
consume_work!(budget)
|
|
143
|
+
render_nodes(children, context.merge(variable => item), output, budget)
|
|
144
|
+
end
|
|
145
|
+
when :if
|
|
146
|
+
expression, truthy, falsy = parts
|
|
147
|
+
render_nodes(resolve(expression, context) ? truthy : falsy, context, output, budget)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def consume_work!(budget)
|
|
153
|
+
budget[0] -= 1
|
|
154
|
+
raise Error, "template rendering exceeds the work limit" if budget[0].negative?
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def append!(output, value)
|
|
158
|
+
raise Error, "rendered output exceeds the byte limit" if output.bytesize + value.bytesize > MAX_OUTPUT_BYTES
|
|
159
|
+
|
|
160
|
+
output << value
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def resolve(path, context)
|
|
164
|
+
segments = path.split(".")
|
|
165
|
+
segments.drop(1).reduce(context.fetch(segments.first)) do |value, segment|
|
|
166
|
+
raise Error, "#{path} does not resolve to template data" unless value.is_a?(Hash)
|
|
167
|
+
value.fetch(segment)
|
|
168
|
+
end
|
|
169
|
+
rescue KeyError
|
|
170
|
+
raise Error, "missing template data: #{path}"
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require_relative "support"
|
|
5
|
+
|
|
6
|
+
module Netstack
|
|
7
|
+
module Configuration
|
|
8
|
+
# 不可变模板快照;基于数据库的模板版本管理由宿主负责。
|
|
9
|
+
class TemplateStore
|
|
10
|
+
Error = Class.new(Netstack::InputError)
|
|
11
|
+
ROOT = File.expand_path("../../../resources/configuration", __dir__).freeze
|
|
12
|
+
|
|
13
|
+
# 构造时显式读取随包文件;预加载该类不会读取模板。
|
|
14
|
+
# 覆盖模板经过复制,并按相同目录契约和安全语法校验。
|
|
15
|
+
def initialize(overrides: {})
|
|
16
|
+
supplied = Support.object(overrides, name: "template overrides")
|
|
17
|
+
raise Error, "unknown template override" unless (supplied.keys - Catalog.template_keys).empty?
|
|
18
|
+
|
|
19
|
+
@sources = Catalog.template_keys.to_h do |key|
|
|
20
|
+
source = supplied.fetch(key) { File.read(File.join(ROOT, key), encoding: Encoding::UTF_8) }
|
|
21
|
+
TemplateManager.validate!(source, variables: Catalog.variables_for_template(key))
|
|
22
|
+
[key, source.dup.freeze]
|
|
23
|
+
end.freeze
|
|
24
|
+
freeze
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def fetch(key)
|
|
28
|
+
@sources.fetch(key.to_s) { raise Error, "unknown configuration template" }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def metadata(key)
|
|
32
|
+
source = fetch(key)
|
|
33
|
+
Values.copy({ key: key.to_s, sha256: Digest::SHA256.hexdigest(source), bytes: source.bytesize,
|
|
34
|
+
variables: Catalog.variables_for_template(key) })
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def inspect = "#<#{self.class.name} count=#{@sources.size}>"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|