empeira 0.1.0.pre.alpha.1
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 +7 -0
- data/LICENSE +661 -0
- data/README.md +150 -0
- data/bin/empeira +18 -0
- data/config/defaults.yaml +229 -0
- data/docs/addons.md +331 -0
- data/docs/architecture.md +277 -0
- data/docs/configuration.md +1204 -0
- data/docs/development.md +472 -0
- data/docs/hiera-and-eyaml.md +361 -0
- data/docs/installation.md +280 -0
- data/docs/networking.md +362 -0
- data/docs/nodes.md +642 -0
- data/docs/proxy.md +136 -0
- data/docs/troubleshooting.md +361 -0
- data/lib/empeira/addons/environment.rb +82 -0
- data/lib/empeira/addons/error.rb +14 -0
- data/lib/empeira/addons/installation.rb +119 -0
- data/lib/empeira/addons/manifest.rb +112 -0
- data/lib/empeira/addons/service.rb +50 -0
- data/lib/empeira/addons/storage.rb +94 -0
- data/lib/empeira/agent/acquisition.rb +120 -0
- data/lib/empeira/agent/artifact.rb +41 -0
- data/lib/empeira/agent/authentication.rb +122 -0
- data/lib/empeira/agent/cache.rb +122 -0
- data/lib/empeira/agent/cache_storage.rb +82 -0
- data/lib/empeira/agent/download.rb +84 -0
- data/lib/empeira/agent/native_commands.rb +84 -0
- data/lib/empeira/agent/release_sources.rb +64 -0
- data/lib/empeira/agent/target.rb +66 -0
- data/lib/empeira/agent/versions.rb +29 -0
- data/lib/empeira/application.rb +86 -0
- data/lib/empeira/build.json +5 -0
- data/lib/empeira/build_info.rb +46 -0
- data/lib/empeira/cli/addons.rb +45 -0
- data/lib/empeira/cli/base.rb +73 -0
- data/lib/empeira/cli/completion.rb +42 -0
- data/lib/empeira/cli/config.rb +22 -0
- data/lib/empeira/cli/main.rb +114 -0
- data/lib/empeira/cli/node.rb +87 -0
- data/lib/empeira/cli/progress.rb +169 -0
- data/lib/empeira/cli/proxy.rb +36 -0
- data/lib/empeira/cli/update.rb +23 -0
- data/lib/empeira/completion/addons.rb +93 -0
- data/lib/empeira/completion/bash.rb +62 -0
- data/lib/empeira/completion/catalog.rb +170 -0
- data/lib/empeira/configuration/additional_services.rb +74 -0
- data/lib/empeira/configuration/addons.rb +85 -0
- data/lib/empeira/configuration/agent_schema.rb +163 -0
- data/lib/empeira/configuration/agent_values.rb +54 -0
- data/lib/empeira/configuration/bootstrap_guests.rb +42 -0
- data/lib/empeira/configuration/command_mocks.rb +65 -0
- data/lib/empeira/configuration/display.rb +20 -0
- data/lib/empeira/configuration/dns_rewrites.rb +58 -0
- data/lib/empeira/configuration/document.rb +67 -0
- data/lib/empeira/configuration/eyaml.rb +46 -0
- data/lib/empeira/configuration/hiera_modulepath.rb +40 -0
- data/lib/empeira/configuration/hiera_mount_schema.rb +62 -0
- data/lib/empeira/configuration/hiera_mounts.rb +83 -0
- data/lib/empeira/configuration/image_schema.rb +68 -0
- data/lib/empeira/configuration/loader.rb +70 -0
- data/lib/empeira/configuration/merge.rb +48 -0
- data/lib/empeira/configuration/network_redirects.rb +73 -0
- data/lib/empeira/configuration/node_images.rb +64 -0
- data/lib/empeira/configuration/schema.rb +215 -0
- data/lib/empeira/configuration/server_mounts.rb +106 -0
- data/lib/empeira/configuration/service_configuration.rb +40 -0
- data/lib/empeira/configuration/ssh_preferences.rb +83 -0
- data/lib/empeira/configuration/user.rb +46 -0
- data/lib/empeira/configuration/vm_interfaces.rb +147 -0
- data/lib/empeira/control_plane/additional_configurations.rb +106 -0
- data/lib/empeira/control_plane/browser.rb +73 -0
- data/lib/empeira/control_plane/controller.rb +763 -0
- data/lib/empeira/control_plane/discovery.rb +69 -0
- data/lib/empeira/control_plane/environment.rb +101 -0
- data/lib/empeira/control_plane/files.rb +263 -0
- data/lib/empeira/control_plane/health.rb +104 -0
- data/lib/empeira/control_plane/plan.rb +356 -0
- data/lib/empeira/control_plane/storage.rb +48 -0
- data/lib/empeira/errors.rb +9 -0
- data/lib/empeira/execution/console.rb +45 -0
- data/lib/empeira/execution/diagnostics.rb +210 -0
- data/lib/empeira/execution/launch_error.rb +14 -0
- data/lib/empeira/execution/result.rb +11 -0
- data/lib/empeira/execution/runner.rb +235 -0
- data/lib/empeira/execution/terminal.rb +35 -0
- data/lib/empeira/hostname_pattern.rb +14 -0
- data/lib/empeira/images/configuration.rb +44 -0
- data/lib/empeira/images/identity.rb +30 -0
- data/lib/empeira/images/recipe.rb +22 -0
- data/lib/empeira/images/reference.rb +43 -0
- data/lib/empeira/images/source.rb +19 -0
- data/lib/empeira/immutable.rb +19 -0
- data/lib/empeira/infrastructure/cleanup.rb +328 -0
- data/lib/empeira/infrastructure/definition.rb +59 -0
- data/lib/empeira/infrastructure/owned_files.rb +63 -0
- data/lib/empeira/infrastructure/recovery_inventory.rb +53 -0
- data/lib/empeira/infrastructure/service.rb +237 -0
- data/lib/empeira/infrastructure/status.rb +73 -0
- data/lib/empeira/infrastructure/store.rb +274 -0
- data/lib/empeira/modules/events.rb +67 -0
- data/lib/empeira/modules/git_safety.rb +60 -0
- data/lib/empeira/modules/host_git.rb +91 -0
- data/lib/empeira/modules/image.rb +27 -0
- data/lib/empeira/modules/installer.rb +67 -0
- data/lib/empeira/modules/request.rb +68 -0
- data/lib/empeira/modules/state.rb +87 -0
- data/lib/empeira/modules/storage.rb +35 -0
- data/lib/empeira/network/bootstrap_proxy.rb +157 -0
- data/lib/empeira/network/definition.rb +61 -0
- data/lib/empeira/network/direct_egress.rb +133 -0
- data/lib/empeira/network/egress.rb +27 -0
- data/lib/empeira/network/gateway.rb +79 -0
- data/lib/empeira/network/host_policy.rb +53 -0
- data/lib/empeira/network/naming.rb +28 -0
- data/lib/empeira/network/peer/adapter_image.rb +32 -0
- data/lib/empeira/network/peer/allocation.rb +45 -0
- data/lib/empeira/network/peer/attachment.rb +27 -0
- data/lib/empeira/network/peer/backend.rb +108 -0
- data/lib/empeira/network/peer/channel.rb +133 -0
- data/lib/empeira/network/peer/channel_server.rb +71 -0
- data/lib/empeira/network/peer/docker_adapter.rb +165 -0
- data/lib/empeira/network/peer/helper_security.rb +40 -0
- data/lib/empeira/network/peer/layout.rb +55 -0
- data/lib/empeira/network/peer/linux_podman.rb +114 -0
- data/lib/empeira/network/peer/podman_machine.rb +112 -0
- data/lib/empeira/network/policy.rb +39 -0
- data/lib/empeira/network/proxy_bindings.rb +52 -0
- data/lib/empeira/network/proxy_policy.rb +64 -0
- data/lib/empeira/network/redirects.rb +89 -0
- data/lib/empeira/network/resource.rb +50 -0
- data/lib/empeira/node/agent_installer.rb +31 -0
- data/lib/empeira/node/agent_package.rb +125 -0
- data/lib/empeira/node/agent_repository.rb +160 -0
- data/lib/empeira/node/apt_configuration.rb +85 -0
- data/lib/empeira/node/bootstrap.rb +17 -0
- data/lib/empeira/node/certificates.rb +98 -0
- data/lib/empeira/node/command_mock.rb +38 -0
- data/lib/empeira/node/command_mocks.rb +114 -0
- data/lib/empeira/node/container.rb +134 -0
- data/lib/empeira/node/container_agent_bootstrap.rb +43 -0
- data/lib/empeira/node/container_bootstrap.rb +52 -0
- data/lib/empeira/node/container_guest.rb +42 -0
- data/lib/empeira/node/container_lifecycle.rb +145 -0
- data/lib/empeira/node/container_preparation.rb +144 -0
- data/lib/empeira/node/container_state.rb +129 -0
- data/lib/empeira/node/definition.rb +20 -0
- data/lib/empeira/node/diagnostic_command.rb +19 -0
- data/lib/empeira/node/dnf_agent_repository.rb +202 -0
- data/lib/empeira/node/external_fact.rb +21 -0
- data/lib/empeira/node/image.rb +38 -0
- data/lib/empeira/node/interactive_tools.rb +33 -0
- data/lib/empeira/node/interface.rb +18 -0
- data/lib/empeira/node/inventory.rb +143 -0
- data/lib/empeira/node/package_bootstrap.rb +129 -0
- data/lib/empeira/node/package_proxy.rb +102 -0
- data/lib/empeira/node/package_sources.rb +19 -0
- data/lib/empeira/node/puppet_command.rb +12 -0
- data/lib/empeira/node/rpm_configuration.rb +62 -0
- data/lib/empeira/node/run_request.rb +52 -0
- data/lib/empeira/node/runtime_proxy.rb +86 -0
- data/lib/empeira/node/service.rb +72 -0
- data/lib/empeira/node/ssh_client.rb +25 -0
- data/lib/empeira/node/ssh_credentials.rb +78 -0
- data/lib/empeira/node/ssh_endpoint.rb +58 -0
- data/lib/empeira/node/ssh_identity.rb +37 -0
- data/lib/empeira/node/user_ssh.rb +60 -0
- data/lib/empeira/node/vm.rb +89 -0
- data/lib/empeira/node/vm_bootstrap.rb +53 -0
- data/lib/empeira/node/vm_lifecycle.rb +212 -0
- data/lib/empeira/node/vm_preflight.rb +37 -0
- data/lib/empeira/node/vm_preparation.rb +161 -0
- data/lib/empeira/node/vm_state.rb +161 -0
- data/lib/empeira/platform/facts.rb +66 -0
- data/lib/empeira/platform/kvm.rb +91 -0
- data/lib/empeira/platform/locations.rb +51 -0
- data/lib/empeira/platform/resolvers.rb +85 -0
- data/lib/empeira/platform/routes.rb +50 -0
- data/lib/empeira/progress.rb +70 -0
- data/lib/empeira/project_root.rb +47 -0
- data/lib/empeira/providers/lifecycle.rb +96 -0
- data/lib/empeira/providers/registry.rb +21 -0
- data/lib/empeira/providers/resource.rb +32 -0
- data/lib/empeira/providers.rb +55 -0
- data/lib/empeira/runtime/agent_helper.rb +34 -0
- data/lib/empeira/runtime/container.rb +184 -0
- data/lib/empeira/runtime/diagnostic_execution.rb +52 -0
- data/lib/empeira/runtime/docker.rb +87 -0
- data/lib/empeira/runtime/image_metadata.rb +83 -0
- data/lib/empeira/runtime/interface.rb +8 -0
- data/lib/empeira/runtime/mount_probe.rb +57 -0
- data/lib/empeira/runtime/podman.rb +122 -0
- data/lib/empeira/runtime/recipe_build.rb +103 -0
- data/lib/empeira/runtime/registry_diagnostics.rb +23 -0
- data/lib/empeira/runtime/service_arguments.rb +55 -0
- data/lib/empeira/runtime/service_images.rb +118 -0
- data/lib/empeira/runtime/service_inspection.rb +159 -0
- data/lib/empeira/runtime/services.rb +120 -0
- data/lib/empeira/runtime/update_helper.rb +97 -0
- data/lib/empeira/runtime/workspace_network.rb +69 -0
- data/lib/empeira/server/environment_configuration.rb +53 -0
- data/lib/empeira/server/http.rb +38 -0
- data/lib/empeira/server/puppetdb_runtime.rb +74 -0
- data/lib/empeira/server/relay_certificate.rb +76 -0
- data/lib/empeira/server/runtime.rb +90 -0
- data/lib/empeira/services/definition.rb +42 -0
- data/lib/empeira/updates/images.rb +104 -0
- data/lib/empeira/updates/modules.rb +45 -0
- data/lib/empeira/updates/service.rb +45 -0
- data/lib/empeira/version_requirement.rb +23 -0
- data/lib/empeira/vm/agent.rb +66 -0
- data/lib/empeira/vm/bootstrap_requirements.rb +69 -0
- data/lib/empeira/vm/certificate_runtime.rb +30 -0
- data/lib/empeira/vm/cloud_init.rb +171 -0
- data/lib/empeira/vm/disk.rb +136 -0
- data/lib/empeira/vm/guest.rb +123 -0
- data/lib/empeira/vm/guest_channel.rb +110 -0
- data/lib/empeira/vm/guest_network.rb +94 -0
- data/lib/empeira/vm/guest_output.rb +52 -0
- data/lib/empeira/vm/guest_shell.rb +79 -0
- data/lib/empeira/vm/guest_upload.rb +43 -0
- data/lib/empeira/vm/image_cache.rb +77 -0
- data/lib/empeira/vm/image_source.rb +91 -0
- data/lib/empeira/vm/interface.rb +8 -0
- data/lib/empeira/vm/interface_ownership.rb +124 -0
- data/lib/empeira/vm/interface_routes.rb +101 -0
- data/lib/empeira/vm/interfaces.rb +240 -0
- data/lib/empeira/vm/management.rb +73 -0
- data/lib/empeira/vm/prerequisites.rb +42 -0
- data/lib/empeira/vm/qemu.rb +83 -0
- data/lib/empeira/vm/qemu_runtime.rb +404 -0
- data/lib/empeira/vm/removal.rb +30 -0
- data/lib/empeira/vm/root_disk.rb +92 -0
- data/lib/empeira/vm/session_lock.rb +56 -0
- data/lib/empeira/vm/system_ssh.rb +29 -0
- data/lib/empeira/workspace.rb +42 -0
- data/lib/empeira.rb +209 -0
- data/resources/browser/relay.rb +22 -0
- data/resources/gateway/Containerfile +8 -0
- data/resources/gateway/bridge.rb +66 -0
- data/resources/gateway/configure.rb +323 -0
- data/resources/modules/Containerfile +15 -0
- data/resources/modules/Gemfile +5 -0
- data/resources/modules/Gemfile.lock +105 -0
- data/resources/modules/worker.rb +161 -0
- data/resources/network/adapter/Containerfile +13 -0
- data/resources/network/adapter/bridge_linux.go +51 -0
- data/resources/network/adapter/framing.go +45 -0
- data/resources/network/adapter/framing_test.go +53 -0
- data/resources/network/adapter/go.mod +3 -0
- data/resources/network/adapter/main_linux.go +273 -0
- data/resources/network/channel.rb +18 -0
- data/resources/network/ssh_connect.rb +32 -0
- data/resources/nodes/apt_sources.rb +66 -0
- data/resources/nodes/debian/Containerfile +10 -0
- data/resources/nodes/diagnostic_exec.rb +109 -0
- data/resources/nodes/interactive_tools.rb +139 -0
- data/resources/nodes/managed_file.rb +91 -0
- data/resources/nodes/management/guest_agent.py +415 -0
- data/resources/nodes/management/setup.sh +20 -0
- data/resources/nodes/management/shell_rc.sh +12 -0
- data/resources/nodes/management/system_account.sh +24 -0
- data/resources/nodes/management/system_home.sh +15 -0
- data/resources/nodes/rpm/Containerfile +10 -0
- data/resources/nodes/runtime_proxy.rb +244 -0
- data/resources/nodes/ssh/proxy.rb +16 -0
- data/resources/nodes/ssh/sshd_config +15 -0
- data/resources/nodes/ssh/start +6 -0
- data/resources/proxy/Containerfile +10 -0
- data/resources/relay/Containerfile +5 -0
- data/resources/relay/puppetdb.rb +81 -0
- data/resources/relay/server_environment.rb +10 -0
- data/resources/relay/server_start.sh +41 -0
- metadata +326 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ipaddr'
|
|
4
|
+
require 'socket'
|
|
5
|
+
|
|
6
|
+
module Empeira
|
|
7
|
+
module Network
|
|
8
|
+
module DirectEgress
|
|
9
|
+
DNS_LABEL = '[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?'
|
|
10
|
+
DNS_NAME = /\A#{DNS_LABEL}(?:\.#{DNS_LABEL})*\z/
|
|
11
|
+
MAX_HOSTS = 256
|
|
12
|
+
BENCHMARK = IPAddr.new('198.18.0.0/15').freeze
|
|
13
|
+
|
|
14
|
+
Resolved = Data.define(:entries) do
|
|
15
|
+
def fingerprint
|
|
16
|
+
Infrastructure::Definition.fingerprint(entries)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def configuration
|
|
20
|
+
JSON.generate('version' => 1, 'entries' => entries)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
def validate!(value, path = 'network.egress')
|
|
27
|
+
return legacy!(value, path) if value.is_a?(Hash)
|
|
28
|
+
unless value.is_a?(Array) && value.size <= MAX_HOSTS
|
|
29
|
+
raise ConfigurationError, "#{path} must be an array with at most #{MAX_HOSTS} destination entries"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
hosts = value.each_with_index.map { |entry, index| validate_entry!(entry, "#{path}[#{index}]") }
|
|
33
|
+
raise ConfigurationError, "#{path} contains duplicate destinations" unless hosts.uniq.size == hosts.size
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def legacy!(value, path)
|
|
37
|
+
return if value.keys == ['mode'] && %w[none proxy].include?(value['mode'])
|
|
38
|
+
|
|
39
|
+
raise ConfigurationError, "#{path} must be a host array (legacy mode accepts only none or proxy)"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def destinations(value)
|
|
43
|
+
return [] unless value.is_a?(Array)
|
|
44
|
+
|
|
45
|
+
value.map { |entry| entry.fetch('host') { entry.fetch('ip') } }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def validate_entry!(entry, path)
|
|
49
|
+
unless entry.is_a?(Hash) && [%w[host ports], %w[ip ports]].include?(entry.keys.sort)
|
|
50
|
+
raise ConfigurationError, "#{path} must contain ports and exactly one of host or ip"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
entry.key?('host') ? validate_host!(entry['host'], path) : validate_ip!(entry['ip'], path)
|
|
54
|
+
validate_ports!(entry['ports'], path)
|
|
55
|
+
entry.slice('host', 'ip')
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def validate_host!(host, path)
|
|
59
|
+
if host.is_a?(String) && host.size <= 253 && host.match?(DNS_NAME) &&
|
|
60
|
+
host.include?('.') && !host.match?(/\A[\d.]+\z/)
|
|
61
|
+
return
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
raise ConfigurationError, "#{path}.host must be a lowercase DNS hostname"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# rubocop:disable-next Metrics/CyclomaticComplexity -- Exact IPv4 syntax and scoped benchmark exception share validation.
|
|
68
|
+
def validate_ip!(value, path, allow_benchmark: false)
|
|
69
|
+
raise IPAddr::InvalidAddressError unless value.is_a?(String) && !value.include?('/')
|
|
70
|
+
|
|
71
|
+
ip = IPAddr.new(value)
|
|
72
|
+
raise ConfigurationError, "#{path}.ip: IPv6 is not supported; the workspace is IPv4-only" if ip.ipv6?
|
|
73
|
+
if BENCHMARK.include?(ip) && !allow_benchmark
|
|
74
|
+
raise ConfigurationError, "#{path}.ip: benchmark addresses are reserved for validated redirect sources"
|
|
75
|
+
end
|
|
76
|
+
return if ip.to_s == value
|
|
77
|
+
|
|
78
|
+
raise ConfigurationError, "#{path}.ip must be an explicit IPv4 address"
|
|
79
|
+
rescue IPAddr::InvalidAddressError
|
|
80
|
+
raise ConfigurationError, "#{path}.ip must be an explicit IPv4 address", cause: nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def validate_ports!(ports, path)
|
|
84
|
+
valid = ports.is_a?(Array) && !ports.empty? && ports.uniq.size == ports.size &&
|
|
85
|
+
ports.all? { |port| port.is_a?(Integer) && port.between?(1, 65_535) }
|
|
86
|
+
return if valid
|
|
87
|
+
|
|
88
|
+
raise ConfigurationError, "#{path}.ports must contain unique TCP ports from 1 through 65535"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
class Resolver
|
|
92
|
+
def initialize(lookup: nil, logger: nil)
|
|
93
|
+
@lookup = lookup || method(:addresses)
|
|
94
|
+
@logger = logger
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def resolve(config)
|
|
98
|
+
entries = config.is_a?(Array) ? config : []
|
|
99
|
+
return Resolved.new([].freeze) if entries.empty?
|
|
100
|
+
|
|
101
|
+
resolved = entries.map { |entry| resolve_entry(entry) }.sort_by { |entry| entry['host'] || entry['ip'] }
|
|
102
|
+
Resolved.new(resolved.freeze)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
# rubocop:disable-next Metrics/AbcSize -- Resolve one complete nonsecret destination including the benchmark exclusion.
|
|
108
|
+
def resolve_entry(entry)
|
|
109
|
+
host = entry['host']
|
|
110
|
+
addresses = entry.key?('ip') ? [entry.fetch('ip')] : ipv4_addresses(host)
|
|
111
|
+
if addresses.any? { |address| BENCHMARK.include?(address) }
|
|
112
|
+
raise Error, 'Direct-egress resolution includes a benchmark address reserved for redirect sources'
|
|
113
|
+
end
|
|
114
|
+
raise Error, "Direct-egress host did not resolve to a supported IPv4 address: #{host}" if addresses.empty?
|
|
115
|
+
|
|
116
|
+
ports = entry.fetch('ports').sort
|
|
117
|
+
@logger&.call(operation: 'direct_egress_resolution', host: host, addresses: addresses, ports: ports)
|
|
118
|
+
entry.slice('host', 'ip').merge('addresses' => addresses, 'ports' => ports).freeze
|
|
119
|
+
rescue IPAddr::InvalidAddressError, SocketError
|
|
120
|
+
raise Error, "Direct-egress host could not be resolved: #{host}", cause: nil
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def ipv4_addresses(host)
|
|
124
|
+
@lookup.call(host).map { |value| IPAddr.new(value) }.select(&:ipv4?).map(&:to_s).uniq.sort
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def addresses(host)
|
|
128
|
+
Socket.getaddrinfo(host, nil, Socket::AF_UNSPEC, Socket::SOCK_STREAM).map { |entry| entry[3] }
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Network
|
|
5
|
+
class Egress < Definition
|
|
6
|
+
def identity
|
|
7
|
+
"#{workspace.id}:egress"
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def backend_name
|
|
11
|
+
"empeira-#{workspace.id}-egress"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def ownership_labels
|
|
15
|
+
super.merge('io.empeira.purpose' => 'proxy-egress-network')
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def verify_isolation!(resource)
|
|
19
|
+
raise UnsupportedPolicy, 'Proxy egress network unexpectedly isolated' unless resource && !resource.isolated
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def to_h
|
|
23
|
+
super.merge('policy' => { 'isolated' => false, 'proxy_only' => true })
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Network
|
|
5
|
+
# One policy, addressing and TCP translation boundary for every peer backend.
|
|
6
|
+
class Gateway
|
|
7
|
+
OFFSETS = { 'gateway' => 2, 'dns' => 3, 'proxy' => 4, 'bootstrap-proxy' => 5 }.freeze
|
|
8
|
+
EXECUTABLE = '/usr/local/libexec/empeira-gateway'
|
|
9
|
+
|
|
10
|
+
def self.address(subnet, key = 'gateway')
|
|
11
|
+
Peer::Layout.new(subnet).address(OFFSETS.fetch(key))
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# rubocop:disable-next Metrics/AbcSize -- Compose the complete provider-independent firewall input.
|
|
15
|
+
def self.plan(state:, resolved:, upstreams:, routes:, additional: nil, redirects: [])
|
|
16
|
+
subnet = state.fetch('peer_network').fetch('subnet')
|
|
17
|
+
resolvers = (upstreams + routes.values.flatten + additional_addresses(additional)).uniq.sort
|
|
18
|
+
unless resolvers.all? { |address| IPAddr.new(address).ipv4? }
|
|
19
|
+
raise Error, 'Gateway is IPv4-only; configure reachable IPv4 DNS upstreams'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
{ 'version' => 1, 'subnet' => subnet, 'gateway' => address(subnet),
|
|
23
|
+
'dns' => address(subnet, 'dns'), 'resolvers' => resolvers,
|
|
24
|
+
'proxies' => %w[proxy bootstrap-proxy].map { |key| address(subnet, key) },
|
|
25
|
+
'blocked' => blocked_addresses(state), 'entries' => resolved.entries, 'redirects' => redirects }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.blocked_addresses(state)
|
|
29
|
+
state.fetch('nodes', {}).values.filter_map do |node|
|
|
30
|
+
node.dig('peer', 'ip') || node.dig('definition', 'ip') if node['network_phase'] == 'bootstrap'
|
|
31
|
+
end.sort
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.additional_addresses(value)
|
|
35
|
+
return [] if value.nil? || value.end_with?('.empeira.internal')
|
|
36
|
+
|
|
37
|
+
return [value] if value.match?(/\A[\d.]+\z/)
|
|
38
|
+
|
|
39
|
+
Socket.getaddrinfo(value, nil, Socket::AF_INET, Socket::SOCK_STREAM).map { |entry| entry[3] }.uniq
|
|
40
|
+
rescue SocketError
|
|
41
|
+
raise Error, "Gateway DNS resolution failed for additional resolver #{value}", cause: nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def initialize(context:, runtime:, state:)
|
|
45
|
+
@context = context
|
|
46
|
+
@runtime = runtime
|
|
47
|
+
@state = state
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def route(resource)
|
|
51
|
+
subnet = @state.fetch('peer_network').fetch('subnet')
|
|
52
|
+
@runtime.configure_workspace_route(resource, gateway: self.class.address(subnet),
|
|
53
|
+
image: ControlPlane::Plan.new(context: @context)
|
|
54
|
+
.gateway_artifact.fetch(:image))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def update_phase_plan(ip, bootstrap:)
|
|
58
|
+
files = ControlPlane::Files.new(context: @context)
|
|
59
|
+
plan = JSON.parse(File.binread(files.path('gateway.json')))
|
|
60
|
+
plan['blocked'] = (plan.fetch('blocked') - [ip] + (bootstrap ? [ip] : [])).sort
|
|
61
|
+
files.write('gateway.json', JSON.generate(plan))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Bootstrap exclusion is changed inside the gateway, before a new node starts.
|
|
65
|
+
def phase(record, bootstrap:)
|
|
66
|
+
identity = Services::Definition.new(key: 'gateway', workspace: @context.workspace)
|
|
67
|
+
resource = @runtime.inspect_service(identity,
|
|
68
|
+
expected_id: @state.dig('control_plane', 'services', 'gateway', 'id'))
|
|
69
|
+
raise Error, 'Gateway is unavailable; run empeira up' unless resource && resource['state'] == 'running'
|
|
70
|
+
|
|
71
|
+
ip = record.dig('peer', 'ip') || record.dig('definition', 'ip')
|
|
72
|
+
result = @runtime.service_exec(resource, [EXECUTABLE, bootstrap ? 'block' : 'unblock', ip])
|
|
73
|
+
raise Error, 'Gateway bootstrap firewall reconcile failed' unless result.success?
|
|
74
|
+
|
|
75
|
+
update_phase_plan(ip, bootstrap: bootstrap)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Network
|
|
5
|
+
class HostPolicy
|
|
6
|
+
def self.validate!(rules, path = 'proxy.rules')
|
|
7
|
+
raise ConfigurationError, "#{path} must be an array" unless rules.is_a?(Array)
|
|
8
|
+
|
|
9
|
+
rules.each_with_index { |rule, index| validate_rule!(rule, "#{path}.#{index}") }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.validate_rule!(rule, location)
|
|
13
|
+
raise ConfigurationError, "#{location} requires hosts and allow arrays" unless valid_rule_keys?(rule)
|
|
14
|
+
unless valid_hosts?(rule['hosts'])
|
|
15
|
+
raise ConfigurationError, "#{location}.hosts must contain lowercase hostname globs using * and ?"
|
|
16
|
+
end
|
|
17
|
+
return if rule['allow'].is_a?(Array) && rule['allow'].all? { |v| ProxyPolicy.valid_domain?(v) }
|
|
18
|
+
|
|
19
|
+
raise ConfigurationError, "#{location}.allow must contain DNS domains or *.domain entries"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.valid_rule_keys?(rule)
|
|
23
|
+
rule.is_a?(Hash) && rule.keys.sort_by(&:to_s) == %w[allow hosts]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.valid_hosts?(hosts)
|
|
27
|
+
hosts.is_a?(Array) && !hosts.empty? && hosts.all? { |v| valid_glob?(v) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.valid_glob?(value)
|
|
31
|
+
HostnamePattern.valid?(value) && value == value.downcase
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def initialize(config)
|
|
35
|
+
@config = config
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.matches?(pattern, hostname)
|
|
39
|
+
HostnamePattern.matches?(pattern, hostname)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def matching(hostname)
|
|
43
|
+
@config.fetch('rules').select do |rule|
|
|
44
|
+
rule.fetch('hosts').any? { |pattern| HostnamePattern.matches?(pattern, hostname) }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def resolve(hostname)
|
|
49
|
+
(@config.fetch('global') + matching(hostname).flat_map { |rule| rule.fetch('allow') }).uniq
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Network
|
|
5
|
+
# Logical identities remain independent of runtime container names and addresses.
|
|
6
|
+
class Naming
|
|
7
|
+
DOMAIN = 'empeira.internal'
|
|
8
|
+
|
|
9
|
+
def hostname(service)
|
|
10
|
+
"#{service}.#{DOMAIN}"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def aliases(service)
|
|
14
|
+
[service, hostname(service)]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def reserved(config)
|
|
18
|
+
additional = config.dig('containers', 'additional').map { |entry| entry.fetch('name') }
|
|
19
|
+
Configuration::AdditionalServices::RESERVED + additional
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def no_proxy(additional = [])
|
|
23
|
+
['localhost', '127.0.0.1', '::1', ".#{DOMAIN}", 'server', 'puppetdb', 'postgres', 'dns', 'proxy',
|
|
24
|
+
*additional].uniq.join(',')
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Network
|
|
5
|
+
module Peer
|
|
6
|
+
class AdapterImage
|
|
7
|
+
attr_reader :image, :recipe, :files
|
|
8
|
+
|
|
9
|
+
# rubocop:disable-next Metrics/AbcSize -- Assemble the reviewed source and resolved builder identity together.
|
|
10
|
+
def initialize(context:)
|
|
11
|
+
root = Pathname(__dir__).join('../../../../resources/network/adapter').realpath
|
|
12
|
+
@files = source_files(root)
|
|
13
|
+
builder = Images::Configuration.reference(context.configuration.dig('images', 'network_adapter_builder'),
|
|
14
|
+
registry: context.configuration.dig('images', 'registry'))
|
|
15
|
+
digest = Infrastructure::Definition.fingerprint(@files)
|
|
16
|
+
@recipe = root.join('Containerfile').read.gsub('@BUILDER@', builder) + "\n# source-sha256: #{digest}\n"
|
|
17
|
+
@image = Images::Configuration.local_image(@recipe, purpose: 'peer-adapter')
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def source_files(root)
|
|
21
|
+
files = root.children.select { |path| path.extname == '.go' || path.basename.to_s == 'go.mod' }
|
|
22
|
+
.to_h { |path| [path.basename.to_s, path.binread] }
|
|
23
|
+
files.merge('LICENSE' => Pathname(__dir__).join('../../../../LICENSE').read)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def ensure!(runtime)
|
|
27
|
+
runtime.ensure_image(image, recipe: recipe, files: files)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Network
|
|
5
|
+
module Peer
|
|
6
|
+
class Allocation
|
|
7
|
+
def initialize(context:, runtime:, runner:)
|
|
8
|
+
@context = context
|
|
9
|
+
@runtime = runtime
|
|
10
|
+
@runner = runner
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def choose
|
|
14
|
+
occupied = @runtime.network_subnets + host_routes
|
|
15
|
+
candidates.find { |candidate| occupied.none? { |route| overlap?(candidate, route) } } ||
|
|
16
|
+
raise(Error, 'No safe private peer subnet is available; inspect host/VPN and runtime routes')
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def candidates
|
|
22
|
+
offset = Digest::SHA256.hexdigest(@context.workspace.id)[0, 4].to_i(16)
|
|
23
|
+
candidates = (0...256).map { |i| "10.#{160 + ((offset + i) % 64)}.#{(offset + i) % 256}.0/24" }
|
|
24
|
+
candidates + alternatives(offset)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def alternatives(offset)
|
|
28
|
+
(16..31).map { |i| "172.#{i}.#{offset % 256}.0/24" } +
|
|
29
|
+
(0...256).map { |i| "192.168.#{(offset + i) % 256}.0/24" }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def overlap?(candidate, route)
|
|
33
|
+
left, right = [candidate, route].map { |value| IPAddr.new(value) }
|
|
34
|
+
left.include?(right.to_range.first) || right.include?(left.to_range.first)
|
|
35
|
+
rescue IPAddr::InvalidAddressError
|
|
36
|
+
raise Error, 'Cannot verify route overlap for the peer network', cause: nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def host_routes
|
|
40
|
+
Platform::Routes.new(platform: @context.platform, runner: @runner).ipv4
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Network
|
|
5
|
+
module Peer
|
|
6
|
+
class Attachment
|
|
7
|
+
def initialize(backend:, record:, state:)
|
|
8
|
+
@backend = backend
|
|
9
|
+
@record = record
|
|
10
|
+
@state = state
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def arguments
|
|
14
|
+
@backend.arguments(@record)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def launch(executable, arguments)
|
|
18
|
+
@backend.launch(executable, arguments)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def connect
|
|
22
|
+
@backend.connect(@record, @state)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Network
|
|
5
|
+
module Peer
|
|
6
|
+
class Backend
|
|
7
|
+
attr_reader :context, :runtime, :runner, :store
|
|
8
|
+
|
|
9
|
+
def self.build(context:, **)
|
|
10
|
+
klass = if context.container_engine == 'docker'
|
|
11
|
+
DockerAdapter
|
|
12
|
+
elsif context.platform.os == :macos
|
|
13
|
+
PodmanMachine
|
|
14
|
+
else
|
|
15
|
+
LinuxPodman
|
|
16
|
+
end
|
|
17
|
+
klass.new(context: context, **)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(context:, runtime:, runner:, store:)
|
|
21
|
+
@context = context
|
|
22
|
+
@runtime = runtime
|
|
23
|
+
@runner = runner
|
|
24
|
+
@store = store
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def key
|
|
28
|
+
self.class.name.split('::').last
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def preflight(state)
|
|
32
|
+
raise Error, 'Old VM network state requires explicit teardown and recreation' unless state['peer_network']
|
|
33
|
+
|
|
34
|
+
resource = owned_network(state)
|
|
35
|
+
details = runtime.network_details(resource.id)
|
|
36
|
+
verify_subnet(details, state.fetch('peer_network').fetch('subnet'))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def owned_network(state)
|
|
40
|
+
definition = Network::Definition.new(workspace: context.workspace, policy: Network::Policy.new)
|
|
41
|
+
resource = runtime.inspect_network(identifier: definition.backend_name)
|
|
42
|
+
definition.verify_ownership!(resource, expected_id: state.dig('resources', 'network', 'id'))
|
|
43
|
+
definition.verify_isolation!(resource)
|
|
44
|
+
raise Error, 'Owned peer network is missing; run empeira up' unless resource
|
|
45
|
+
|
|
46
|
+
resource
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def verify_subnet(details, expected)
|
|
50
|
+
subnets = details['subnets'] || details.dig('IPAM', 'Config') || []
|
|
51
|
+
return if subnets.any? { |entry| (entry['subnet'] || entry['Subnet']) == expected }
|
|
52
|
+
|
|
53
|
+
raise Providers::OwnershipError, 'Observed peer subnet differs from workspace inventory'
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def network_name
|
|
57
|
+
Network::Definition.new(workspace: context.workspace, policy: Network::Policy.new).backend_name
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def bridge
|
|
61
|
+
"ep#{context.workspace.id[0, 10]}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def tap(record)
|
|
65
|
+
"et#{record.fetch('peer').fetch('token')[0, 12]}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def command(executable, *arguments, timeout: 30)
|
|
69
|
+
result = runner.run(executable, arguments: arguments, timeout: timeout)
|
|
70
|
+
return result.stdout if result.success?
|
|
71
|
+
|
|
72
|
+
raise Error,
|
|
73
|
+
"Peer network #{executable} operation failed (exit=#{result.exit_status}, timeout=#{result.timed_out})"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def check_record(record)
|
|
77
|
+
return if record.dig('peer', 'backend') == key
|
|
78
|
+
|
|
79
|
+
raise Error, 'Peer backend differs from the node instance; restore its runtime and platform'
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def health(records)
|
|
83
|
+
records.to_h { |record| [record.fetch('hostname'), healthy?(record)] }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def system_address(record, port)
|
|
87
|
+
Configuration::SSHPreferences.port!(port)
|
|
88
|
+
check_record(record)
|
|
89
|
+
address = IPAddr.new(record.fetch('peer').fetch('ip'))
|
|
90
|
+
subnet = system_subnet
|
|
91
|
+
unless address.ipv4? && address.private? && subnet.include?(address)
|
|
92
|
+
raise Providers::OwnershipError, 'System SSH target is outside the owned peer subnet'
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
address.to_s
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def system_subnet
|
|
99
|
+
state = store.load
|
|
100
|
+
resource = owned_network(state)
|
|
101
|
+
subnet = state.fetch('peer_network').fetch('subnet')
|
|
102
|
+
verify_subnet(runtime.network_details(resource.id), subnet)
|
|
103
|
+
IPAddr.new(subnet)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Network
|
|
5
|
+
module Peer
|
|
6
|
+
# Control is private and token-bound. The packet stream never carries application configuration.
|
|
7
|
+
class Channel
|
|
8
|
+
def initialize(context:, runner:, record:)
|
|
9
|
+
@runner = runner
|
|
10
|
+
@token = record.fetch('peer').fetch('token')
|
|
11
|
+
paths = [context.locations.temporary, context.locations.home].map { |root| root.join("ep-#{@token[0, 24]}") }
|
|
12
|
+
@directory = paths.find { |path| path.join('ethernet.sock').to_s.bytesize < 100 }
|
|
13
|
+
raise Error, 'No short private path is available for the peer packet channel' unless @directory
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def socket
|
|
17
|
+
@directory.join('ethernet.sock').to_s
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def prepare
|
|
21
|
+
FileUtils.mkdir_p(@directory, mode: 0o700)
|
|
22
|
+
verify_directory
|
|
23
|
+
raise Error, 'Peer channel is still active; stop its owning VM before retrying' if live?
|
|
24
|
+
|
|
25
|
+
%w[control.sock ethernet.sock].each { |name| remove_socket(@directory.join(name)) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def start(command)
|
|
29
|
+
launch(command)
|
|
30
|
+
Timeout.timeout(10) do
|
|
31
|
+
loop do
|
|
32
|
+
return if healthy?
|
|
33
|
+
|
|
34
|
+
sleep 0.1
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
rescue Timeout::Error
|
|
38
|
+
raise Error, 'Peer packet channel did not become ready; inspect adapter/runtime availability'
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def healthy?
|
|
42
|
+
reply = request('status')
|
|
43
|
+
reply['token'] == @token && reply['running'] == true
|
|
44
|
+
rescue SystemCallError, IOError, Timeout::Error, JSON::ParserError
|
|
45
|
+
false
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def stop
|
|
49
|
+
return unless @directory.exist?
|
|
50
|
+
|
|
51
|
+
verify_directory
|
|
52
|
+
if live?
|
|
53
|
+
reply = request('stop')
|
|
54
|
+
raise Providers::OwnershipError, 'Peer channel ownership changed' unless reply['token'] == @token
|
|
55
|
+
|
|
56
|
+
Timeout.timeout(15) { sleep 0.1 while live? }
|
|
57
|
+
end
|
|
58
|
+
%w[control.sock ethernet.sock].each { |name| remove_socket(@directory.join(name)) }
|
|
59
|
+
rescue SystemCallError, Timeout::Error
|
|
60
|
+
raise Error, 'Peer channel shutdown is uncertain; resources retained for recovery', cause: nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def destroy
|
|
64
|
+
stop
|
|
65
|
+
FileUtils.remove_entry_secure(@directory) if @directory.exist?
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def launch(command)
|
|
71
|
+
config = @directory.join('channel.json')
|
|
72
|
+
File.write(config, JSON.generate('token' => @token, 'command' => command), mode: 'w', perm: 0o600)
|
|
73
|
+
helper = Pathname(__dir__).join('../../../../resources/network/channel.rb').realpath
|
|
74
|
+
result = @runner.run(RbConfig.ruby, arguments: [helper.to_s, config.to_s], timeout: 10)
|
|
75
|
+
raise Error, 'Cannot launch peer packet channel' unless result.success?
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def request(operation)
|
|
79
|
+
Timeout.timeout(2) do
|
|
80
|
+
UNIXSocket.open(@directory.join('control.sock')) do |connection|
|
|
81
|
+
connection.puts(JSON.generate('token' => @token, 'operation' => operation))
|
|
82
|
+
JSON.parse(connection.gets(4096))
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def live?
|
|
88
|
+
path = @directory.join('channel.pid')
|
|
89
|
+
return false unless path.file?
|
|
90
|
+
|
|
91
|
+
Process.kill(0, Integer(path.read))
|
|
92
|
+
true
|
|
93
|
+
rescue Errno::ESRCH
|
|
94
|
+
false
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def verify_directory
|
|
98
|
+
metadata = @directory.lstat
|
|
99
|
+
unless metadata.directory? && metadata.uid == Process.uid && metadata.mode.nobits?(0o077)
|
|
100
|
+
raise Providers::OwnershipError, 'Peer channel directory is not private to its owner'
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
verify_saved_token
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def verify_saved_token
|
|
107
|
+
path = @directory.join('channel.json')
|
|
108
|
+
return unless path.exist? || path.symlink?
|
|
109
|
+
|
|
110
|
+
metadata = path.lstat
|
|
111
|
+
unless private_file?(metadata) && JSON.parse(path.read)['token'] == @token
|
|
112
|
+
raise Providers::OwnershipError, 'Peer channel ownership token changed; cleanup refused'
|
|
113
|
+
end
|
|
114
|
+
rescue JSON::ParserError
|
|
115
|
+
raise Providers::OwnershipError, 'Invalid peer channel ownership file', cause: nil
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def private_file?(metadata)
|
|
119
|
+
metadata.file? && metadata.uid == Process.uid && metadata.mode.nobits?(0o077)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def remove_socket(path)
|
|
123
|
+
return unless path.exist? || path.symlink?
|
|
124
|
+
unless path.lstat.socket? && path.lstat.uid == Process.uid
|
|
125
|
+
raise Providers::OwnershipError, 'Unexpected file at the peer socket path'
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
File.unlink(path)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|