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,19 @@
|
|
|
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
|
+
module Vendors
|
|
8
|
+
class Radware
|
|
9
|
+
class Topology < Netstack::Connector::Topology::Strategy
|
|
10
|
+
# Alteon 只支持读取端口描述,不提供邻居发现或自动改写。
|
|
11
|
+
def self.supports?(capability) = capability == :interface_descriptions
|
|
12
|
+
|
|
13
|
+
# 从设备配置转储解析端口名称。
|
|
14
|
+
def description_template = "radware_port_names.textfsm"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
module Vendors
|
|
8
|
+
class Radware < Netstack::Connector::Device
|
|
9
|
+
vendor :radware
|
|
10
|
+
# 声明菜单式 CLI 的命令、提示、超时、诊断和确认规则。
|
|
11
|
+
profile do
|
|
12
|
+
running_config_strategy Netstack::Connector::Vendors::Radware::RunningConfig
|
|
13
|
+
tftp_strategy Netstack::Connector::Vendors::Radware::TftpBackup
|
|
14
|
+
topology_strategy Netstack::Connector::Vendors::Radware::Topology
|
|
15
|
+
commands do
|
|
16
|
+
running_config "/cfg/dump"
|
|
17
|
+
save_config "/cfg/save"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
prompts do
|
|
21
|
+
login(/(?:\A|(?<=[\r\n]))(?:>>[^\r\n]*#|(?!>>)[^\r\n]+[>#])\s*\z/)
|
|
22
|
+
command(/(?:\A|(?<=[\r\n]))[^\r\n]+#\s*\z/)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
errors do
|
|
26
|
+
command(/^[ \t]*(?:Unknown|Invalid|Error)\s+command\b/i)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
command_timeout 30
|
|
30
|
+
|
|
31
|
+
interactions do
|
|
32
|
+
confirm %r{Confirm\s+Sync\s+to\s+Peer\s+\[y/n\]:\s*\z}i, response: "y\n"
|
|
33
|
+
confirm %r{Synchronize\s+configuration\s+changes\?\s+\[y/n\]:\s*\z}i, response: "y\n"
|
|
34
|
+
confirm %r{Confirm\s+saving\s+to\s+FLASH\s+\[y/n\]:\s*\z}i, response: "y\n"
|
|
35
|
+
confirm %r{Confirm\s+operation\s+without\s+applying\s+changes\s+
|
|
36
|
+
\(leave\s+them\s+pending\)\s+\[y/n\]:\s*\z}ix, response: "y\n"
|
|
37
|
+
confirm %r{Confirm\s+operation\s+without\s+saving\s+changes\s+
|
|
38
|
+
\(leave\s+them\s+pending\)\s+\[y/n\]:?\s*\z}ix, response: "y\n"
|
|
39
|
+
confirm(/Confirm\s+seeing\s+above\s+note\s+\[y\]:\s*\z/i, response: "y\n")
|
|
40
|
+
confirm(/Please\s+enter\s+y\s+to\s+perform\s+the\s+action,\s+n\s+to\s+skip\s+it:\s*\z/i,
|
|
41
|
+
response: "y\n"
|
|
42
|
+
)
|
|
43
|
+
confirm %r{Display\s+private\s+keys\?\s+\[y/n\]:\s*\z}i, response: "n\n"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# 连接有效期间返回登录横幅中的 HA 状态。
|
|
48
|
+
def ha_state = connected? ? @ha_state : nil
|
|
49
|
+
|
|
50
|
+
protected
|
|
51
|
+
|
|
52
|
+
# 从登录横幅提取 HA 状态并冻结,供连接期间查询。
|
|
53
|
+
def after_login(_session, response)
|
|
54
|
+
@ha_state = response.raw[/^\s*HA\s+State:\s*(\S+)/i, 1]&.freeze
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../netstack" unless defined?(Netstack::LOADER)
|
|
4
|
+
|
|
5
|
+
module Netstack
|
|
6
|
+
# 单设备会话、厂商语法及配置操作;不读取清单或持久化执行任务。
|
|
7
|
+
module Connector
|
|
8
|
+
VENDORS = {
|
|
9
|
+
h3c: "H3c", h3c_wireless: "H3cWireless", cisco_ios: "CiscoIos",
|
|
10
|
+
cisco_nxos: "CiscoNxos", radware: "Radware", palo_alto: "PaloAlto",
|
|
11
|
+
huawei: "Huawei", hillstone: "Hillstone"
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
def self.vendors = VENDORS.keys
|
|
15
|
+
|
|
16
|
+
def self.vendor_class(vendor)
|
|
17
|
+
key = vendor.to_s.downcase.tr(" -", "__").to_sym
|
|
18
|
+
name = VENDORS.fetch(key) { raise ArgumentError, "unsupported vendor: #{vendor.inspect}" }
|
|
19
|
+
Vendors.const_get(name, false)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.build(vendor, **settings)
|
|
23
|
+
vendor_class(vendor).new(**settings)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.open(vendor, **settings, &)
|
|
27
|
+
raise ArgumentError, "open requires a block" unless block_given?
|
|
28
|
+
|
|
29
|
+
vendor_class(vendor).open(**settings, &)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Ddi
|
|
5
|
+
# 只有开始检查点已持久化,才应用范围内的有序意图。
|
|
6
|
+
# 恢复通过读取当前状态并绑定新建父对象 ID 推进,不重放写入。
|
|
7
|
+
# 变更已验证与 DNS/DHCP 服务已部署分别表达。
|
|
8
|
+
class Applier
|
|
9
|
+
def initialize(client:, settings:)
|
|
10
|
+
@settings = settings
|
|
11
|
+
@state = State.new(client: client, settings: settings)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call(plan:, execution_id:, checkpoint:)
|
|
15
|
+
recover(plan: plan, execution_id: execution_id, events: [], checkpoint: checkpoint)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def recover(plan:, execution_id:, events:, checkpoint:)
|
|
19
|
+
raise InputError, "checkpoint must be callable" unless checkpoint.respond_to?(:call)
|
|
20
|
+
id = Values.text(execution_id, name: "execution_id")
|
|
21
|
+
raise InputError, "DDI plan belongs to a different scope" unless plan.scope == @settings.to_h
|
|
22
|
+
groups = validate_events!(plan, id, events)
|
|
23
|
+
plan.operations.each_with_index do |operation, index|
|
|
24
|
+
history = groups.fetch(index, [])
|
|
25
|
+
next if history.last&.fetch("stage") == "verified"
|
|
26
|
+
operation = bind_parent(plan, operation, index, groups)
|
|
27
|
+
current = @state.read(operation.fetch("kind"), operation.fetch("identity"))
|
|
28
|
+
base = { "execution_id" => id, "plan_digest" => plan.digest, "operation" => index }
|
|
29
|
+
if history.empty?
|
|
30
|
+
unless current == operation["before"]
|
|
31
|
+
raise Netstack::Error.new("DDI object changed after planning", code: :state_conflict, stage: :preflight)
|
|
32
|
+
end
|
|
33
|
+
persist!(checkpoint, base.merge("stage" => "started"))
|
|
34
|
+
receipt = @state.write(operation)
|
|
35
|
+
persist!(checkpoint, base.merge("stage" => "receipt", "receipt" => receipt))
|
|
36
|
+
current = @state.read(operation.fetch("kind"), operation.fetch("identity"))
|
|
37
|
+
else
|
|
38
|
+
receipt = history.find { |event| event["stage"] == "receipt" }&.fetch("receipt")
|
|
39
|
+
end
|
|
40
|
+
expected_id = receipt.is_a?(Integer) ? receipt : operation["desired"] && operation.dig("before", "id")
|
|
41
|
+
unless @state.matches?(current, operation["desired"]) && (!expected_id || current&.fetch("id", nil) == expected_id)
|
|
42
|
+
raise Netstack::Error.new("DDI write outcome needs reconciliation", code: :unknown_result, stage: :verification,
|
|
43
|
+
details: { "operation" => index })
|
|
44
|
+
end
|
|
45
|
+
verified = base.merge("stage" => "verified", "observed" => current)
|
|
46
|
+
persist!(checkpoint, verified)
|
|
47
|
+
groups[index] = [verified]
|
|
48
|
+
end
|
|
49
|
+
Values.copy({ "execution_id" => id, "plan_digest" => plan.digest, "state" => "verified" })
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def bind_parent(plan, operation, index, groups)
|
|
55
|
+
return operation unless %w[role record].include?(operation["kind"])
|
|
56
|
+
identity = operation.fetch("identity")
|
|
57
|
+
parent_index = (0...index).to_a.reverse.find do |candidate|
|
|
58
|
+
previous = plan.operations[candidate]
|
|
59
|
+
previous["kind"] == "zone" && previous["desired"] && previous["identity"].slice("view_id", "name") == identity.slice("view_id", "name")
|
|
60
|
+
end
|
|
61
|
+
return operation unless parent_index
|
|
62
|
+
observed = groups.fetch(parent_index).last.fetch("observed")
|
|
63
|
+
operation.merge("identity" => identity.merge("zone_id" => observed.fetch("id")))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def validate_events!(plan, execution_id, events)
|
|
67
|
+
raise InputError, "DDI checkpoints must be an array" unless events.is_a?(Array)
|
|
68
|
+
groups = {}
|
|
69
|
+
events.each do |event|
|
|
70
|
+
unless event.is_a?(Hash) && event["execution_id"] == execution_id && event["plan_digest"] == plan.digest && event["operation"].is_a?(Integer) && event["operation"].between?(0, plan.operations.length - 1)
|
|
71
|
+
raise InputError, "DDI checkpoints do not match this execution"
|
|
72
|
+
end
|
|
73
|
+
index = event["operation"]
|
|
74
|
+
if index.positive? && groups[index - 1]&.last&.fetch("stage") != "verified"
|
|
75
|
+
raise InputError, "DDI operations are out of order"
|
|
76
|
+
end
|
|
77
|
+
history = (groups[index] ||= [])
|
|
78
|
+
stages = history.map { |item| item["stage"] } + [event["stage"]]
|
|
79
|
+
unless [%w[started], %w[started receipt], %w[started verified], %w[started receipt verified]].include?(stages)
|
|
80
|
+
raise InputError, "Invalid DDI checkpoint stage"
|
|
81
|
+
end
|
|
82
|
+
if event["stage"] == "receipt" && event["receipt"] != true && !(event["receipt"].is_a?(Integer) && event["receipt"].positive?)
|
|
83
|
+
raise InputError, "Invalid DDI receipt"
|
|
84
|
+
end
|
|
85
|
+
if event["stage"] == "verified"
|
|
86
|
+
operation = plan.operations[index]
|
|
87
|
+
receipt = history.find { |item| item["stage"] == "receipt" }&.fetch("receipt")
|
|
88
|
+
expected_id = receipt.is_a?(Integer) ? receipt : operation["desired"] && operation.dig("before", "id")
|
|
89
|
+
unless @state.matches?(event["observed"], operation["desired"]) && (!expected_id || event.dig("observed", "id") == expected_id)
|
|
90
|
+
raise InputError, "DDI verification does not match its receipt"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
history << event
|
|
94
|
+
end
|
|
95
|
+
groups
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def persist!(checkpoint, event)
|
|
99
|
+
raise Netstack::Error.new("DDI checkpoint was not committed", code: :checkpoint_failed, stage: :checkpoint) unless checkpoint.call(Values.copy(event)) == true
|
|
100
|
+
rescue Netstack::Error
|
|
101
|
+
raise
|
|
102
|
+
rescue StandardError
|
|
103
|
+
raise Netstack::Error.new("DDI checkpoint failed", code: :checkpoint_failed, stage: :checkpoint), cause: nil
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Ddi
|
|
5
|
+
# Legacy v1 exposes server-wide status, not a submission ID. Require an
|
|
6
|
+
# observed active phase after our receipt before accepting DONE.
|
|
7
|
+
class Deployment
|
|
8
|
+
ACTIVE = [-1, 0, 1].freeze
|
|
9
|
+
FAILED = [2, 3, 4, 6].freeze
|
|
10
|
+
|
|
11
|
+
def initialize(client:, settings:)
|
|
12
|
+
@client = client
|
|
13
|
+
@settings = settings
|
|
14
|
+
@scope = Scope.new(client: client, settings: settings)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def submit(plan:, mutation_result:, checkpoint:, events: [])
|
|
18
|
+
raise InputError, "checkpoint must be callable" unless checkpoint.respond_to?(:call)
|
|
19
|
+
unless plan.scope == @settings.to_h && mutation_result.is_a?(Hash) && mutation_result["state"] == "verified" && mutation_result["plan_digest"] == plan.digest
|
|
20
|
+
raise InputError, "Deployment requires verified DDI writes in this scope"
|
|
21
|
+
end
|
|
22
|
+
id = Values.text(mutation_result["execution_id"], name: "execution_id")
|
|
23
|
+
groups = validate_events!(plan, id, events)
|
|
24
|
+
return [] if plan.operations.empty? || plan.services.empty?
|
|
25
|
+
raise InputError, "No deployment servers configured" if @settings.server_ids.empty?
|
|
26
|
+
emitted = []
|
|
27
|
+
@settings.server_ids.each do |server_id|
|
|
28
|
+
history = groups[server_id]
|
|
29
|
+
if history
|
|
30
|
+
raise Netstack::Error.new("Deployment submission outcome is unknown; do not replay it", code: :unknown_result) if history.last["stage"] == "started"
|
|
31
|
+
raise Netstack::Error.new("Deployment needs review before further submissions", code: :deployment_failed) if %w[failed warning].include?(history.last["stage"])
|
|
32
|
+
next
|
|
33
|
+
end
|
|
34
|
+
status = status(server_id)
|
|
35
|
+
raise Netstack::Error.new("Another deployment is active", code: :deployment_busy) if ACTIVE.include?(status)
|
|
36
|
+
base = { "execution_id" => id, "plan_digest" => plan.digest, "server_id" => server_id, "services" => plan.services }
|
|
37
|
+
persist!(checkpoint, base.merge("stage" => "started"), emitted)
|
|
38
|
+
@client.write(:post, "deployServerServices", query: { serverId: server_id, services: "services=#{plan.services.join(",")}" })
|
|
39
|
+
persist!(checkpoint, base.merge("stage" => "receipt"), emitted)
|
|
40
|
+
end
|
|
41
|
+
Values.copy(emitted)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def recover(plan:, execution_id:, events:, checkpoint:)
|
|
45
|
+
raise InputError, "checkpoint must be callable" unless checkpoint.respond_to?(:call)
|
|
46
|
+
raise InputError, "DDI deployment scope changed" unless plan.scope == @settings.to_h
|
|
47
|
+
execution_id = Values.text(execution_id, name: "execution_id")
|
|
48
|
+
groups = validate_events!(plan, execution_id, events)
|
|
49
|
+
return {} if plan.operations.empty? || plan.services.empty?
|
|
50
|
+
result = {}
|
|
51
|
+
@settings.server_ids.each do |server_id|
|
|
52
|
+
history = groups[server_id]
|
|
53
|
+
unless history
|
|
54
|
+
result[server_id] = "not_submitted"
|
|
55
|
+
next
|
|
56
|
+
end
|
|
57
|
+
last = history.last
|
|
58
|
+
if %w[verified warning failed].include?(last["stage"])
|
|
59
|
+
result[server_id] = last["stage"]
|
|
60
|
+
next
|
|
61
|
+
end
|
|
62
|
+
current = status(server_id)
|
|
63
|
+
# A lost submission receipt cannot be correlated through this API.
|
|
64
|
+
if last["stage"] == "started"
|
|
65
|
+
result[server_id] = "unknown"
|
|
66
|
+
next
|
|
67
|
+
end
|
|
68
|
+
if ACTIVE.include?(current)
|
|
69
|
+
persist!(checkpoint, last.merge("stage" => "active", "status" => current), []) unless last["stage"] == "active"
|
|
70
|
+
result[server_id] = "pending"
|
|
71
|
+
elsif current == 7 && last["stage"] == "active"
|
|
72
|
+
persist!(checkpoint, last.merge("stage" => "verified", "status" => current), [])
|
|
73
|
+
result[server_id] = "verified"
|
|
74
|
+
elsif FAILED.include?(current) && last["stage"] == "active"
|
|
75
|
+
persist!(checkpoint, last.merge("stage" => "failed", "status" => current), [])
|
|
76
|
+
result[server_id] = "failed"
|
|
77
|
+
elsif current == 5 && last["stage"] == "active"
|
|
78
|
+
persist!(checkpoint, last.merge("stage" => "warning", "status" => current), [])
|
|
79
|
+
result[server_id] = "warning"
|
|
80
|
+
else
|
|
81
|
+
result[server_id] = "unknown"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
Values.copy(result)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def validate_events!(plan, execution_id, events)
|
|
90
|
+
raise InputError, "Invalid deployment checkpoints" unless events.is_a?(Array)
|
|
91
|
+
raise InputError, "Empty plans cannot have deployment checkpoints" if !events.empty? && (plan.operations.empty? || plan.services.empty?)
|
|
92
|
+
groups = {}
|
|
93
|
+
events.each do |event|
|
|
94
|
+
unless event.is_a?(Hash) && event["execution_id"] == execution_id && event["plan_digest"] == plan.digest && @settings.server_ids.include?(event["server_id"]) && event["services"] == plan.services
|
|
95
|
+
raise InputError, "Deployment checkpoints do not match this execution"
|
|
96
|
+
end
|
|
97
|
+
server_id = event["server_id"]
|
|
98
|
+
group = (groups[server_id] ||= [])
|
|
99
|
+
stage = event["stage"]
|
|
100
|
+
valid_stage = case group.last&.fetch("stage")
|
|
101
|
+
when nil then stage == "started"
|
|
102
|
+
when "started" then stage == "receipt"
|
|
103
|
+
when "receipt" then stage == "active" && ACTIVE.include?(event["status"])
|
|
104
|
+
when "active" then (stage == "verified" && event["status"] == 7) || (stage == "warning" && event["status"] == 5) || (stage == "failed" && FAILED.include?(event["status"]))
|
|
105
|
+
else false
|
|
106
|
+
end
|
|
107
|
+
raise InputError, "Invalid deployment checkpoint sequence" unless valid_stage
|
|
108
|
+
if stage == "started"
|
|
109
|
+
preceding = @settings.server_ids.take_while { |id| id != server_id }
|
|
110
|
+
unless preceding.all? { |id| groups[id]&.any? { |item| item["stage"] == "receipt" } }
|
|
111
|
+
raise InputError, "Deployment submissions are out of order"
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
group << event
|
|
115
|
+
end
|
|
116
|
+
groups
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def status(server_id)
|
|
120
|
+
@scope.entity!(server_id, types: ["Server"])
|
|
121
|
+
value = @client.json("getServerDeploymentStatus", serverId: server_id)
|
|
122
|
+
raise Netstack::Error.new("Invalid deployment status", code: :invalid_response) unless value.is_a?(Integer) && (-1..8).cover?(value)
|
|
123
|
+
value
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def persist!(checkpoint, event, events)
|
|
127
|
+
event = Values.copy(event)
|
|
128
|
+
raise Netstack::Error.new("Deployment checkpoint was not committed", code: :checkpoint_failed, stage: :checkpoint) unless checkpoint.call(event) == true
|
|
129
|
+
events << event
|
|
130
|
+
rescue Netstack::Error
|
|
131
|
+
raise
|
|
132
|
+
rescue StandardError
|
|
133
|
+
raise Netstack::Error.new("Deployment checkpoint failed", code: :checkpoint_failed, stage: :checkpoint), cause: nil
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ipaddr"
|
|
4
|
+
|
|
5
|
+
module Netstack
|
|
6
|
+
module Ddi
|
|
7
|
+
module Inputs
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def id(value) = Values.integer(value, name: "BlueCat object ID", min: 1)
|
|
11
|
+
|
|
12
|
+
def domain(value)
|
|
13
|
+
value = Values.text(value, name: "domain").downcase.delete_suffix(".")
|
|
14
|
+
unless value.length <= 253 && value.split(".", -1).length >= 2 && value.split(".", -1).all? { |label| label.match?(/\A[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\z/) }
|
|
15
|
+
raise InputError, "Invalid DNS name"
|
|
16
|
+
end
|
|
17
|
+
value
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def ipv4(value)
|
|
21
|
+
value = Values.text(value, name: "IPv4 address")
|
|
22
|
+
address = IPAddr.new(value)
|
|
23
|
+
raise InputError, "Invalid IPv4 address" unless address.ipv4? && !value.include?("/")
|
|
24
|
+
address.to_s
|
|
25
|
+
rescue IPAddr::InvalidAddressError
|
|
26
|
+
raise InputError, "Invalid IPv4 address"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def network(value)
|
|
30
|
+
value = Values.text(value, name: "IPv4 CIDR")
|
|
31
|
+
parts = value.split("/", -1)
|
|
32
|
+
raise InputError, "IPv4 CIDR must contain one address and one prefix" unless parts.size == 2
|
|
33
|
+
address, prefix = parts
|
|
34
|
+
prefix = Values.integer(prefix, name: "IPv4 prefix", min: 0, max: 30)
|
|
35
|
+
network = IPAddr.new("#{ipv4(address)}/#{prefix}")
|
|
36
|
+
raise InputError, "CIDR must be an aligned IPv4 network" unless network.to_s == ipv4(address)
|
|
37
|
+
network
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def cidr(value)
|
|
41
|
+
network = network(value)
|
|
42
|
+
"#{network}/#{network.prefix}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def mac(value)
|
|
46
|
+
value = Values.text(value, name: "MAC address").downcase.tr("-", ":")
|
|
47
|
+
raise InputError, "Invalid MAC address" unless value.match?(/\A(?:[0-9a-f]{2}:){5}[0-9a-f]{2}\z/)
|
|
48
|
+
value
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def properties(value)
|
|
52
|
+
raise Netstack::Error.new("Invalid BlueCat properties", code: :invalid_response) unless value.is_a?(String)
|
|
53
|
+
value.split(/(?<!\\)\|/).each_with_object({}) do |part, result|
|
|
54
|
+
key, content = part.split("=", 2)
|
|
55
|
+
raise Netstack::Error.new("Invalid BlueCat properties", code: :invalid_response) unless key && content && !result.key?(key)
|
|
56
|
+
result[key] = content.gsub("\\|", "|")
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
# 限定范围的 DNS/DHCP/IPAM 意图;BlueCat 传输与宿主事务各自独立。
|
|
5
|
+
module Ddi
|
|
6
|
+
# Produces a reviewable intent from exact current state; planning never writes.
|
|
7
|
+
class Manager
|
|
8
|
+
def initialize(client:, settings:)
|
|
9
|
+
@client = client
|
|
10
|
+
@settings = settings
|
|
11
|
+
@state = State.new(client: client, settings: settings)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def dns_zone(name:, replace: false)
|
|
15
|
+
dns_plan(name: name, replace: replace)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def dns_record(name:, value:, type: "A", replace: false)
|
|
19
|
+
raise InputError, "Only A and CNAME records are supported" unless %w[A CNAME].include?(type)
|
|
20
|
+
value = type == "A" ? Inputs.ipv4(value) : Inputs.domain(value)
|
|
21
|
+
dns_plan(name: name, replace: replace, record: { "type" => type, "rdata" => value })
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def dns_forward(name:, roles:, replace: false)
|
|
25
|
+
roles = Settings.roles(roles)
|
|
26
|
+
raise InputError, "Forward zones require a FORWARDER role" unless roles.any? { |role| role["type"] == "FORWARDER" }
|
|
27
|
+
dns_plan(name: name, replace: replace, roles: roles)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def delete_zone(name:, recursive: false)
|
|
31
|
+
raise InputError, "Zone deletion requires explicit recursive intent" unless recursive == true
|
|
32
|
+
name = Inputs.domain(name)
|
|
33
|
+
require_views!
|
|
34
|
+
operations = @settings.views.filter_map do |view|
|
|
35
|
+
operation("zone", { "view_id" => view["id"], "name" => name }, nil, delete: true)
|
|
36
|
+
end
|
|
37
|
+
plan(operations: operations, services: ["DNS"])
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def blacklist(name:, present: true)
|
|
41
|
+
raise InputError, "Blacklist policy is not configured" unless @settings.policy_id
|
|
42
|
+
raise InputError, "Blacklist present must be boolean" unless [true, false].include?(present)
|
|
43
|
+
identity = { "name" => Inputs.domain(name), "policy_id" => @settings.policy_id }
|
|
44
|
+
op = operation("blacklist", identity, present ? { "present" => true } : nil, delete: !present)
|
|
45
|
+
plan(operations: [op].compact, services: ["DNS"])
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def block(cidr:, parent_id: @settings.configuration_id)
|
|
49
|
+
cidr = Inputs.cidr(cidr)
|
|
50
|
+
parent_id = Inputs.id(parent_id)
|
|
51
|
+
validate_container!(parent_id, cidr) unless parent_id == @settings.configuration_id
|
|
52
|
+
op = operation("block", { "parent_id" => parent_id, "cidr" => cidr }, { "cidr" => cidr })
|
|
53
|
+
plan(operations: [op].compact, services: [])
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def network(cidr:, block_id:, gateway:)
|
|
57
|
+
cidr, block_id, gateway = Inputs.cidr(cidr), Inputs.id(block_id), Inputs.ipv4(gateway)
|
|
58
|
+
validate_container!(block_id, cidr)
|
|
59
|
+
usable!(Inputs.network(cidr), gateway)
|
|
60
|
+
op = operation("network", { "parent_id" => block_id, "cidr" => cidr }, { "cidr" => cidr, "gateway" => gateway })
|
|
61
|
+
plan(operations: [op].compact, services: ["DHCP"])
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def delete_network(cidr:, block_id:)
|
|
65
|
+
identity = { "parent_id" => Inputs.id(block_id), "cidr" => Inputs.cidr(cidr) }
|
|
66
|
+
op = operation("network", identity, nil, delete: true)
|
|
67
|
+
plan(operations: [op].compact, services: ["DHCP"])
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def dhcp_range(cidr:, network_id:, start: nil, end_address: nil, reserve_tail: nil)
|
|
71
|
+
network = Inputs.network(cidr)
|
|
72
|
+
network_id = Inputs.id(network_id)
|
|
73
|
+
row = @client.entity("getEntityById", id: network_id)
|
|
74
|
+
unless row && row["type"] == "IP4Network" && Inputs.properties(row["properties"])["CIDR"] == "#{network}/#{network.prefix}"
|
|
75
|
+
raise InputError, "DHCP network ID does not match the exact CIDR"
|
|
76
|
+
end
|
|
77
|
+
if reserve_tail
|
|
78
|
+
raise InputError, "Specify explicit bounds or reserve_tail" if start || end_address
|
|
79
|
+
tail = Values.integer(reserve_tail, name: "reserved tail", min: 1)
|
|
80
|
+
start = network.to_range.first.succ.to_s
|
|
81
|
+
last = network.to_range.last.to_i - tail
|
|
82
|
+
raise InputError, "Network is too small for the reserved tail" unless last > network.to_i
|
|
83
|
+
end_address = IPAddr.new(last, Socket::AF_INET).to_s
|
|
84
|
+
end
|
|
85
|
+
start, end_address = Inputs.ipv4(start), Inputs.ipv4(end_address)
|
|
86
|
+
usable!(network, start)
|
|
87
|
+
usable!(network, end_address)
|
|
88
|
+
raise InputError, "DHCP range end precedes start" if IPAddr.new(start).to_i > IPAddr.new(end_address).to_i
|
|
89
|
+
gateway = Inputs.properties(row["properties"])["gateway"]
|
|
90
|
+
if gateway && (IPAddr.new(start).to_i..IPAddr.new(end_address).to_i).cover?(IPAddr.new(Inputs.ipv4(gateway)).to_i)
|
|
91
|
+
raise InputError, "DHCP range includes the gateway"
|
|
92
|
+
end
|
|
93
|
+
identity = { "network_id" => network_id, "start" => start, "end" => end_address }
|
|
94
|
+
op = operation("range", identity, identity.slice("start", "end"))
|
|
95
|
+
plan(operations: [op].compact, services: ["DHCP"])
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def fixed_address(ip:, mac:)
|
|
99
|
+
address_plan(ip: ip, mac: Inputs.mac(mac))
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def reserve_address(ip:)
|
|
103
|
+
ip = Inputs.ipv4(ip)
|
|
104
|
+
before = @state.read("address", { "ip" => ip })
|
|
105
|
+
raise InputError, "An existing MAC binding is required" unless before && before["mac"]
|
|
106
|
+
address_plan(ip: ip, mac: Inputs.mac(before["mac"]))
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
def plan(operations:, services:)
|
|
112
|
+
Plan.new(operations: operations, services: services, scope: @settings.to_h)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def dns_plan(name:, replace:, record: nil, roles: nil)
|
|
116
|
+
raise InputError, "replace must be boolean" unless [true, false].include?(replace)
|
|
117
|
+
name = Inputs.domain(name)
|
|
118
|
+
require_views!
|
|
119
|
+
operations = []
|
|
120
|
+
@settings.views.each do |view|
|
|
121
|
+
identity = { "view_id" => view["id"], "name" => name }
|
|
122
|
+
existing = @state.read("zone", identity)
|
|
123
|
+
if replace && existing
|
|
124
|
+
validate_delete!(existing)
|
|
125
|
+
operations << { "kind" => "zone", "identity" => identity, "before" => existing, "desired" => nil }
|
|
126
|
+
end
|
|
127
|
+
fresh = replace || existing.nil?
|
|
128
|
+
desired = { "name" => name, "deployable" => "true" }
|
|
129
|
+
op = fresh ? { "kind" => "zone", "identity" => identity, "before" => nil, "desired" => desired } : operation("zone", identity, desired)
|
|
130
|
+
operations << op if op
|
|
131
|
+
(roles || view["roles"]).each do |role|
|
|
132
|
+
role_identity = identity.merge("interface_id" => role["interface_id"])
|
|
133
|
+
role_identity["zone_id"] = existing["id"] unless fresh
|
|
134
|
+
desired = { "type" => role["type"] }
|
|
135
|
+
op = fresh ? { "kind" => "role", "identity" => role_identity, "before" => nil, "desired" => desired } : operation("role", role_identity, desired)
|
|
136
|
+
operations << op if op
|
|
137
|
+
end
|
|
138
|
+
next unless record
|
|
139
|
+
record_identity = identity.merge("type" => record["type"])
|
|
140
|
+
record_identity["zone_id"] = existing["id"] unless fresh
|
|
141
|
+
unless fresh
|
|
142
|
+
other_type = record["type"] == "A" ? "CNAME" : "A"
|
|
143
|
+
raise InputError, "A and CNAME cannot coexist at the same name" if @state.read("record", identity.merge("type" => other_type))
|
|
144
|
+
end
|
|
145
|
+
op = fresh ? { "kind" => "record", "identity" => record_identity, "before" => nil, "desired" => record } : operation("record", record_identity, record)
|
|
146
|
+
operations << op if op
|
|
147
|
+
end
|
|
148
|
+
plan(operations: operations, services: ["DNS"])
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def address_plan(ip:, mac:)
|
|
152
|
+
ip = Inputs.ipv4(ip)
|
|
153
|
+
network = @client.entity("getIPRangedByIP", containerId: @settings.configuration_id, type: "IP4Network", address: ip)
|
|
154
|
+
raise InputError, "Address does not belong to a managed network" unless network && network["type"] == "IP4Network"
|
|
155
|
+
props = Inputs.properties(network["properties"])
|
|
156
|
+
usable!(Inputs.network(props["CIDR"]), ip)
|
|
157
|
+
raise InputError, "Gateway cannot be reserved as a client address" if props["gateway"] == ip
|
|
158
|
+
identity = { "ip" => ip }
|
|
159
|
+
before = @state.read("address", identity)
|
|
160
|
+
raise InputError, "Address is bound to another MAC" if before && before["mac"] && before["mac"] != mac
|
|
161
|
+
desired = { "mac" => mac, "state" => "DHCP_RESERVED" }
|
|
162
|
+
op = @state.matches?(before, desired) ? nil : { "kind" => "address", "identity" => identity, "before" => before, "desired" => desired }
|
|
163
|
+
plan(operations: [op].compact, services: ["DHCP"])
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def operation(kind, identity, desired, delete: false)
|
|
167
|
+
before = @state.read(kind, identity)
|
|
168
|
+
return nil if @state.matches?(before, desired)
|
|
169
|
+
validate_delete!(before) if delete && before && kind != "blacklist"
|
|
170
|
+
raise Netstack::Error.new("Existing DDI object conflicts with requested state", code: :state_conflict) if before && !delete
|
|
171
|
+
{ "kind" => kind, "identity" => identity, "before" => before, "desired" => desired }
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def validate_delete!(before)
|
|
175
|
+
@state.validate_delete!(before.fetch("id"))
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def validate_container!(id, cidr)
|
|
179
|
+
row = @client.entity("getEntityById", id: id)
|
|
180
|
+
raise InputError, "Parent is not an IPv4 block" unless row && row["type"] == "IP4Block"
|
|
181
|
+
parent = Inputs.network(Inputs.properties(row["properties"])["CIDR"])
|
|
182
|
+
child = Inputs.network(cidr)
|
|
183
|
+
raise InputError, "Child network must be inside its parent block" unless parent.include?(child) && parent.prefix < child.prefix
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def usable!(network, ip)
|
|
187
|
+
address = IPAddr.new(ip)
|
|
188
|
+
return if network.include?(address) && address != network.to_range.first && address != network.to_range.last
|
|
189
|
+
raise InputError, "Address must be a usable host inside the network"
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def require_views!
|
|
193
|
+
raise InputError, "At least one managed DNS view is required" if @settings.views.empty?
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|