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,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'io/wait'
|
|
4
|
+
|
|
5
|
+
module Empeira
|
|
6
|
+
module Network
|
|
7
|
+
module Peer
|
|
8
|
+
class ChannelServer
|
|
9
|
+
def initialize(path, runner: Execution::Runner.new)
|
|
10
|
+
@directory = Pathname(path).dirname
|
|
11
|
+
@config = JSON.parse(File.read(path))
|
|
12
|
+
@runner = runner
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def run
|
|
16
|
+
File.umask(0o077)
|
|
17
|
+
control = @directory.join('control.sock')
|
|
18
|
+
@listener = UNIXServer.new(control)
|
|
19
|
+
File.chmod(0o600, control)
|
|
20
|
+
File.write(@directory.join('channel.pid'), Process.pid.to_s, mode: 'w', perm: 0o600)
|
|
21
|
+
@connection = UNIXSocket.new(@directory.join('ethernet.sock'))
|
|
22
|
+
@worker = Thread.new { forward }
|
|
23
|
+
serve
|
|
24
|
+
ensure
|
|
25
|
+
cleanup
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def cleanup
|
|
31
|
+
begin
|
|
32
|
+
@connection&.shutdown
|
|
33
|
+
rescue StandardError
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
@worker.kill.join if @worker && !@worker.join(10)
|
|
37
|
+
@listener&.close
|
|
38
|
+
FileUtils.rm_f(@directory.join('control.sock'))
|
|
39
|
+
FileUtils.rm_f(@directory.join('channel.pid'))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def forward
|
|
43
|
+
File.open(@directory.join('channel.log'), 'w', 0o600) do |log|
|
|
44
|
+
command = @config.fetch('command')
|
|
45
|
+
@result = @runner.stream(command.first, arguments: command.drop(1),
|
|
46
|
+
input: @connection, output: @connection, error: log)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def serve
|
|
51
|
+
loop do
|
|
52
|
+
break unless @worker.alive?
|
|
53
|
+
next unless @listener.wait_readable(0.2)
|
|
54
|
+
|
|
55
|
+
client = @listener.accept
|
|
56
|
+
begin
|
|
57
|
+
message = JSON.parse(client.gets(4096))
|
|
58
|
+
next unless message['token'] == @config.fetch('token')
|
|
59
|
+
|
|
60
|
+
client.puts(JSON.generate('token' => @config.fetch('token'), 'running' => @worker.alive?))
|
|
61
|
+
stop = message['operation'] == 'stop'
|
|
62
|
+
ensure
|
|
63
|
+
client.close
|
|
64
|
+
end
|
|
65
|
+
break if stop
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Network
|
|
5
|
+
module Peer
|
|
6
|
+
# The same whole-NIC transport serves local Docker and Docker Desktop.
|
|
7
|
+
# rubocop:disable-next Metrics/ClassLength -- Owned NIC and system SSH share adapter security checks.
|
|
8
|
+
class DockerAdapter < Backend
|
|
9
|
+
def preflight(state)
|
|
10
|
+
super
|
|
11
|
+
return if runtime.architecture == context.platform.architecture.to_s
|
|
12
|
+
|
|
13
|
+
raise Error, 'Peer adapter and VM architecture differ; emulation is not supported'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def prepare(record, state)
|
|
17
|
+
check_record(record)
|
|
18
|
+
channel(record).prepare
|
|
19
|
+
@image = AdapterImage.new(context: context)
|
|
20
|
+
@image.ensure!(runtime)
|
|
21
|
+
resource = ensure_adapter(record, state)
|
|
22
|
+
runtime.start_service(resource) unless resource['state'] == 'running'
|
|
23
|
+
verify_adapter(record)
|
|
24
|
+
store.write(state)
|
|
25
|
+
attach_bridge(resource)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def arguments(record)
|
|
29
|
+
['-netdev', "stream,id=peer,addr.type=unix,addr.path=#{channel(record).socket},server=on",
|
|
30
|
+
'-device', "virtio-net-pci,netdev=peer,mac=#{record.fetch('mac_address')}"]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def launch(executable, arguments)
|
|
34
|
+
command(executable, *arguments)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def connect(record, _state)
|
|
38
|
+
File.chmod(0o600, channel(record).socket)
|
|
39
|
+
channel(record).start(packet_command(record))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def stop(record)
|
|
43
|
+
channel(record).stop
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def destroy(record, state)
|
|
47
|
+
channel(record).destroy
|
|
48
|
+
runtime.remove_service(identity(record), expected_id: record.dig('peer', 'adapter_id'))
|
|
49
|
+
record.fetch('peer')['adapter_id'] = nil
|
|
50
|
+
store.write(state)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def healthy?(record)
|
|
54
|
+
channel(record).healthy?
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def system_ssh_command(record, port:)
|
|
58
|
+
address = system_address(record, port)
|
|
59
|
+
@image = AdapterImage.new(context: context)
|
|
60
|
+
verify_adapter(record)
|
|
61
|
+
resource = runtime.inspect_service(identity(record), expected_id: record.dig('peer', 'adapter_id'))
|
|
62
|
+
verify_definition!(record, resource)
|
|
63
|
+
raise Providers::OwnershipError, 'VM peer channel is not active' unless healthy?(record)
|
|
64
|
+
|
|
65
|
+
source = system_source(resource, address)
|
|
66
|
+
[*connector_engine, 'exec', '-i', resource.fetch('id'), '/adapter', 'connect', address, port.to_s, source]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
protected
|
|
70
|
+
|
|
71
|
+
def connector_engine
|
|
72
|
+
[runtime.name]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def system_source(resource, target)
|
|
76
|
+
source = resource.dig('networks', network_name, 'IPAddress')
|
|
77
|
+
unless source.is_a?(String) && IPAddr.new("#{target}/24").include?(IPAddr.new(source))
|
|
78
|
+
raise Providers::OwnershipError, 'Peer connector address is outside its owned subnet'
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
source
|
|
82
|
+
rescue IPAddr::InvalidAddressError
|
|
83
|
+
raise Providers::OwnershipError, 'Peer connector address is unavailable', cause: nil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def capless_adapter_definition(record)
|
|
87
|
+
Services::Definition.new(key: identity(record).key, workspace: context.workspace,
|
|
88
|
+
network: network_name, image: @image.image, memory: 64, cpus: 1,
|
|
89
|
+
command: ['hold'], read_only: true, cap_drop: ['ALL'],
|
|
90
|
+
security_options: ['no-new-privileges'])
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def channel(record)
|
|
94
|
+
Channel.new(context: context, runner: runner, record: record)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def identity(record)
|
|
98
|
+
Services::Definition.new(key: "peer-#{record.fetch('peer').fetch('token')[0, 16]}",
|
|
99
|
+
workspace: context.workspace)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def adapter_definition(record)
|
|
103
|
+
Services::Definition.new(key: identity(record).key, workspace: context.workspace,
|
|
104
|
+
network: network_name, image: @image.image, memory: 64, cpus: 1,
|
|
105
|
+
command: ['hold'], read_only: true, cap_drop: ['ALL'], cap_add: ['NET_ADMIN'],
|
|
106
|
+
devices: ['/dev/net/tun'], security_options: ['no-new-privileges'],
|
|
107
|
+
sysctls: { 'net.ipv4.ip_forward' => '0', 'net.ipv6.conf.all.disable_ipv6' => '1',
|
|
108
|
+
'net.ipv6.conf.default.disable_ipv6' => '1' })
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def ensure_adapter(record, state)
|
|
112
|
+
expected = record.dig('peer', 'adapter_id')
|
|
113
|
+
resource = runtime.inspect_service(identity(record), expected_id: expected)
|
|
114
|
+
verify_definition!(record, resource) if resource
|
|
115
|
+
# The random per-instance token and adapter identity were persisted with the lease.
|
|
116
|
+
resource ||= runtime.create_service(adapter_definition(record))
|
|
117
|
+
record.fetch('peer')['adapter_id'] = resource.fetch('id')
|
|
118
|
+
store.write(state)
|
|
119
|
+
resource
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def verify_definition!(record, resource)
|
|
123
|
+
return if resource.dig('labels', 'io.empeira.definition') == adapter_definition(record).fingerprint
|
|
124
|
+
|
|
125
|
+
raise Providers::OwnershipError, 'Unexpected peer adapter definition'
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def packet_command(record)
|
|
129
|
+
['docker', 'exec', '-i', record.fetch('peer').fetch('adapter_id'), '/adapter', 'tap', tap(record), 'peerbr']
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def validate_exposure!(resource)
|
|
133
|
+
unless resource && resource.fetch('networks').keys == [network_name] && resource.fetch('ports').empty? &&
|
|
134
|
+
resource.fetch('published_ports').values.all?(&:nil?)
|
|
135
|
+
raise Providers::OwnershipError, 'Peer helper has unexpected network or port exposure'
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def attach_bridge(resource)
|
|
140
|
+
result = runtime.service_exec(resource, %w[/adapter bridge peerbr eth0])
|
|
141
|
+
raise Error, 'Cannot attach the Docker peer adapter bridge' unless result.success?
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def binary_digest(resource)
|
|
145
|
+
result = runtime.service_exec(resource, %w[/adapter sha256])
|
|
146
|
+
digest = result.stdout.strip
|
|
147
|
+
return digest if result.success? && digest.match?(/\A[0-9a-f]{64}\z/)
|
|
148
|
+
|
|
149
|
+
raise Error, 'Cannot verify peer adapter binary integrity'
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def verify_adapter(record)
|
|
153
|
+
resource = runtime.inspect_service(identity(record), expected_id: record.dig('peer', 'adapter_id'))
|
|
154
|
+
validate_exposure!(resource)
|
|
155
|
+
HelperSecurity.verify!(resource, privileged_tap: key == 'DockerAdapter')
|
|
156
|
+
digest = binary_digest(resource)
|
|
157
|
+
expected = record.dig('peer', 'helper_sha256')
|
|
158
|
+
raise Providers::OwnershipError, 'Peer helper checksum changed' if expected && expected != digest
|
|
159
|
+
|
|
160
|
+
record.fetch('peer')['helper_sha256'] = digest
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Network
|
|
5
|
+
module Peer
|
|
6
|
+
module HelperSecurity
|
|
7
|
+
def self.verify!(resource, privileged_tap:)
|
|
8
|
+
config = resource.fetch('host_config')
|
|
9
|
+
return if base_valid?(config) && resource.fetch('mounts').empty? &&
|
|
10
|
+
capabilities_valid?(config, privileged_tap) && devices_valid?(config, privileged_tap)
|
|
11
|
+
|
|
12
|
+
raise Providers::OwnershipError, 'Peer helper privileges or mounts differ from the managed definition'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.base_valid?(config)
|
|
16
|
+
config['Privileged'] == false && config['ReadonlyRootfs'] == true &&
|
|
17
|
+
Array(config['SecurityOpt']).include?('no-new-privileges')
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.capabilities_valid?(config, tap)
|
|
21
|
+
caps = Array(config['CapAdd']).map { |cap| cap.delete_prefix('CAP_') }
|
|
22
|
+
dropped = Array(config['CapDrop'])
|
|
23
|
+
return dropped == ['ALL'] && caps == ['NET_ADMIN'] if tap
|
|
24
|
+
|
|
25
|
+
!dropped.empty? && caps.empty?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.devices_valid?(config, tap)
|
|
29
|
+
devices = Array(config['Devices'])
|
|
30
|
+
return devices.empty? unless tap
|
|
31
|
+
|
|
32
|
+
devices.size == 1 && devices.first['PathOnHost'] == '/dev/net/tun' &&
|
|
33
|
+
devices.first['PathInContainer'] == '/dev/net/tun' &&
|
|
34
|
+
config.dig('Sysctls', 'net.ipv4.ip_forward') == '0' &&
|
|
35
|
+
config.dig('Sysctls', 'net.ipv6.conf.all.disable_ipv6') == '1'
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ipaddr'
|
|
4
|
+
require 'securerandom'
|
|
5
|
+
|
|
6
|
+
module Empeira
|
|
7
|
+
module Network
|
|
8
|
+
module Peer
|
|
9
|
+
# Addresses belong to node instances, under the existing workspace mutation lock.
|
|
10
|
+
class Layout
|
|
11
|
+
attr_reader :subnet
|
|
12
|
+
|
|
13
|
+
def initialize(subnet)
|
|
14
|
+
@subnet = subnet
|
|
15
|
+
@range = IPAddr.new(subnet)
|
|
16
|
+
unless @range.ipv4? && @range.private? && @range.prefix == 24 && "#{@range}/24" == subnet
|
|
17
|
+
raise Infrastructure::StateError, 'Invalid workspace peer subnet'
|
|
18
|
+
end
|
|
19
|
+
rescue IPAddr::InvalidAddressError
|
|
20
|
+
raise Infrastructure::StateError, 'Invalid workspace peer subnet', cause: nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def address(offset)
|
|
24
|
+
IPAddr.new(@range.to_i + offset, Socket::AF_INET).to_s
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def container_pool
|
|
28
|
+
"#{address(128)}/25"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def lease(nodes)
|
|
32
|
+
used = nodes.values.filter_map { |record| record.dig('peer', 'ip') }
|
|
33
|
+
ip = (32..95).map { |offset| address(offset) }.find { |candidate| !used.include?(candidate) }
|
|
34
|
+
raise Error, 'Workspace VM address pool exhausted; destroy an unused VM to release its lease' unless ip
|
|
35
|
+
|
|
36
|
+
{ 'ip' => ip, 'token' => SecureRandom.hex(16), 'adapter_id' => nil }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def container_lease(nodes)
|
|
40
|
+
used = nodes.values.filter_map { |record| record.dig('definition', 'ip') }
|
|
41
|
+
ip = (96..127).map { |offset| address(offset) }.find { |candidate| !used.include?(candidate) }
|
|
42
|
+
raise Error, 'Workspace container node pool exhausted; destroy an unused node' unless ip
|
|
43
|
+
|
|
44
|
+
ip
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def valid_lease?(entry)
|
|
48
|
+
entry.is_a?(Hash) && (32..95).any? { |offset| address(offset) == entry['ip'] } &&
|
|
49
|
+
entry['token'].is_a?(String) && entry['token'].match?(/\A[0-9a-f]{32}\z/) &&
|
|
50
|
+
Node::Inventory.valid_id?(entry['adapter_id'])
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Network
|
|
5
|
+
module Peer
|
|
6
|
+
class LinuxPodman < DockerAdapter
|
|
7
|
+
def preflight(state)
|
|
8
|
+
super
|
|
9
|
+
info = JSON.parse(command('podman', '--remote=false', 'info', '--format', 'json'))
|
|
10
|
+
unless info.dig('host', 'security', 'rootless') && info.dig('host', 'networkBackend') == 'netavark'
|
|
11
|
+
raise Error, 'VM peer networking requires local rootless Podman/Netavark; no rootful fallback'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
namespace('test', '-r', '/dev/net/tun')
|
|
15
|
+
namespace('test', '-w', '/dev/net/tun')
|
|
16
|
+
namespace('test', '-r', '/dev/kvm')
|
|
17
|
+
namespace('test', '-w', '/dev/kvm')
|
|
18
|
+
verify_forwarding
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def prepare(record, state)
|
|
22
|
+
check_record(record)
|
|
23
|
+
verify_tap_absent!(record)
|
|
24
|
+
@image = AdapterImage.new(context: context)
|
|
25
|
+
@image.ensure!(runtime)
|
|
26
|
+
resource = ensure_adapter(record, state)
|
|
27
|
+
runtime.start_service(resource) unless resource['state'] == 'running'
|
|
28
|
+
verify_adapter(record)
|
|
29
|
+
store.write(state)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def arguments(record)
|
|
33
|
+
['-netdev', "tap,id=peer,ifname=#{tap(record)},script=no,downscript=no",
|
|
34
|
+
'-device', "virtio-net-pci,netdev=peer,mac=#{record.fetch('mac_address')}"]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def launch(executable, arguments)
|
|
38
|
+
namespace(executable, *arguments)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def connect(record, _state)
|
|
42
|
+
# QEMU owns the nonpersistent TAP FD. Closing it removes the interface.
|
|
43
|
+
namespace('ip', 'link', 'set', 'dev', tap(record), 'alias', record.fetch('peer').fetch('token'))
|
|
44
|
+
namespace('ip', 'link', 'set', 'dev', tap(record), 'master', bridge)
|
|
45
|
+
namespace('ip', 'link', 'set', 'dev', tap(record), 'up')
|
|
46
|
+
verify_forwarding
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def stop(record)
|
|
50
|
+
links = JSON.parse(namespace('ip', '-j', 'link', 'show'))
|
|
51
|
+
return unless links.any? { |link| link['ifname'] == tap(record) }
|
|
52
|
+
|
|
53
|
+
raise Error, 'VM TAP is still present after shutdown; refusing to release the node lease'
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def destroy(record, state)
|
|
57
|
+
stop(record)
|
|
58
|
+
runtime.remove_service(identity(record), expected_id: record.dig('peer', 'adapter_id'))
|
|
59
|
+
record.fetch('peer')['adapter_id'] = nil
|
|
60
|
+
store.write(state)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def healthy?(record)
|
|
64
|
+
health([record]).fetch(record.fetch('hostname'))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def health(records)
|
|
68
|
+
return {} if records.empty?
|
|
69
|
+
|
|
70
|
+
links = JSON.parse(namespace('ip', '-j', 'link', 'show', timeout: 2))
|
|
71
|
+
records.to_h { |record| [record.fetch('hostname'), attached?(record, links)] }
|
|
72
|
+
rescue Error, JSON::ParserError
|
|
73
|
+
records.to_h { |record| [record.fetch('hostname'), false] }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
protected
|
|
77
|
+
|
|
78
|
+
def adapter_definition(record)
|
|
79
|
+
capless_adapter_definition(record)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def connector_engine
|
|
83
|
+
%w[podman --remote=false]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def verify_tap_absent!(record)
|
|
89
|
+
links = JSON.parse(namespace('ip', '-j', 'link', 'show'))
|
|
90
|
+
return unless links.any? { |link| link['ifname'] == tap(record) }
|
|
91
|
+
|
|
92
|
+
raise Providers::OwnershipError, 'VM TAP already exists; inspect its owning process'
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def namespace(*, **)
|
|
96
|
+
command('podman', '--remote=false', 'unshare', '--rootless-netns', *, **)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def attached?(record, links)
|
|
100
|
+
links.any? do |link|
|
|
101
|
+
link['ifname'] == tap(record) && link['ifalias'] == record.dig('peer', 'token') &&
|
|
102
|
+
link['master'] == bridge && link.fetch('flags', []).include?('UP')
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def verify_forwarding
|
|
107
|
+
return if namespace('cat', "/proc/sys/net/ipv4/conf/#{bridge}/forwarding").strip == '0'
|
|
108
|
+
|
|
109
|
+
raise Error, 'Peer bridge routing is enabled; refusing VM attachment'
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'shellwords'
|
|
4
|
+
|
|
5
|
+
module Empeira
|
|
6
|
+
module Network
|
|
7
|
+
module Peer
|
|
8
|
+
class PodmanMachine < DockerAdapter
|
|
9
|
+
def preflight(state)
|
|
10
|
+
super
|
|
11
|
+
select_machine
|
|
12
|
+
verify_rootless
|
|
13
|
+
namespace('test', '-w', '/dev/net/tun')
|
|
14
|
+
raise Error, 'Machine peer bridge routing is enabled' unless
|
|
15
|
+
namespace('cat', "/proc/sys/net/ipv4/conf/#{bridge}/forwarding").strip == '0'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def prepare(record, state)
|
|
19
|
+
check_record(record)
|
|
20
|
+
bind_machine(record, state)
|
|
21
|
+
channel(record).prepare
|
|
22
|
+
@image = AdapterImage.new(context: context)
|
|
23
|
+
@image.ensure!(runtime)
|
|
24
|
+
resource = ensure_adapter(record, state)
|
|
25
|
+
runtime.start_service(resource) unless resource['state'] == 'running'
|
|
26
|
+
export_helper(record, resource)
|
|
27
|
+
store.write(state)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
protected
|
|
31
|
+
|
|
32
|
+
def adapter_definition(record)
|
|
33
|
+
capless_adapter_definition(record)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def packet_command(record)
|
|
37
|
+
machine = record.fetch('peer').fetch('machine')
|
|
38
|
+
namespace_command(machine, helper_path(record).to_s, 'tap', tap(record), bridge)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def helper_path(record)
|
|
42
|
+
context.locations.workspace(context.workspace).join('vms', record.fetch('hostname'), 'peer-adapter')
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def bind_machine(record, state)
|
|
48
|
+
if record.dig('peer', 'machine') && record.dig('peer', 'machine') != @machine
|
|
49
|
+
raise Providers::OwnershipError, 'Node belongs to a different Podman Machine'
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
record.fetch('peer')['machine'] = @machine
|
|
53
|
+
store.write(state)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def selected_connection
|
|
57
|
+
connections = JSON.parse(command('podman', 'system', 'connection', 'list', '--format', 'json'))
|
|
58
|
+
connections.select { |entry| entry['Default'] }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def select_machine
|
|
62
|
+
machines = JSON.parse(command('podman', 'machine', 'list', '--format', 'json'))
|
|
63
|
+
active = machines.select { |entry| entry['Running'] }
|
|
64
|
+
selected = selected_connection
|
|
65
|
+
unless active.size == 1 && selected.size == 1 && selected.first['Name'] == active.first['Name']
|
|
66
|
+
raise Error, 'Select one unambiguous running rootless Podman Machine and its default connection'
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
@machine = active.first.fetch('Name')
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def verify_rootless
|
|
73
|
+
info = JSON.parse(command('podman', 'info', '--format', 'json'))
|
|
74
|
+
return if info.dig('host', 'security', 'rootless') && info.dig('host', 'networkBackend') == 'netavark'
|
|
75
|
+
|
|
76
|
+
raise Error, 'Podman Machine peer networking requires rootless Netavark; no rootful fallback'
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def verify_empty_capabilities(resource)
|
|
80
|
+
result = runtime.service_exec(resource, %w[/adapter capabilities])
|
|
81
|
+
return if result.success? && result.stdout.strip.match?(/\A0{16}\z/)
|
|
82
|
+
|
|
83
|
+
raise Providers::OwnershipError, 'Podman staging helper must have no effective capabilities'
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def export_helper(record, resource)
|
|
87
|
+
path = helper_path(record)
|
|
88
|
+
runtime.copy_from(resource, '/adapter', path)
|
|
89
|
+
verify_empty_capabilities(resource)
|
|
90
|
+
digest = verify_adapter(record)
|
|
91
|
+
raise Error, 'Exported peer helper checksum mismatch' unless Digest::SHA256.file(path).hexdigest == digest
|
|
92
|
+
|
|
93
|
+
File.chmod(0o700, path)
|
|
94
|
+
observed = command('podman', 'machine', 'ssh', @machine,
|
|
95
|
+
Shellwords.join(['sha256sum', path.to_s])).split.first
|
|
96
|
+
return if observed == digest
|
|
97
|
+
|
|
98
|
+
raise Error, 'Podman Machine cannot verify the peer helper at its host path; configure its shared mount'
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def namespace(*)
|
|
102
|
+
executable, *arguments = namespace_command(@machine, *)
|
|
103
|
+
command(executable, *arguments)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def namespace_command(machine, *)
|
|
107
|
+
['podman', 'machine', 'ssh', machine, Shellwords.join(['podman', 'unshare', '--rootless-netns', *])]
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rubocop:disable Lint/UnusedMethodArgument -- Preserve explicit adapter keyword contracts.
|
|
4
|
+
|
|
5
|
+
module Empeira
|
|
6
|
+
module Network
|
|
7
|
+
class UnsupportedPolicy < Error; end
|
|
8
|
+
|
|
9
|
+
Policy = Data.define do
|
|
10
|
+
def require_support!(isolated:, controlled_egress:)
|
|
11
|
+
raise UnsupportedPolicy, 'Isolated networking is required' unless isolated == true
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class Interface
|
|
16
|
+
def initialize(context:, runner:)
|
|
17
|
+
@context = context
|
|
18
|
+
@runner = runner
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def provision(policy:)
|
|
22
|
+
policy.require_support!(**capabilities)
|
|
23
|
+
create(policy: policy)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
protected
|
|
27
|
+
|
|
28
|
+
def capabilities
|
|
29
|
+
{ isolated: false, controlled_egress: false }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def create(policy:)
|
|
33
|
+
raise UnavailableFeature, 'This command is not implemented yet.'
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# rubocop:enable Lint/UnusedMethodArgument
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Network
|
|
5
|
+
class ProxyBindings
|
|
6
|
+
def initialize(context:, resources:, network:, nodes:)
|
|
7
|
+
@context = context
|
|
8
|
+
@resources = resources
|
|
9
|
+
@network = network
|
|
10
|
+
@nodes = nodes
|
|
11
|
+
@config = context.configuration.fetch('proxy')
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def configuration
|
|
15
|
+
entries = %w[server].map do |name|
|
|
16
|
+
rules(name.tr('-', '_'), address(name), @config.fetch('global'))
|
|
17
|
+
end
|
|
18
|
+
@nodes.sort.each_with_index do |(hostname, record), index|
|
|
19
|
+
entries << node_rules(hostname, record, index)
|
|
20
|
+
end
|
|
21
|
+
"#{entries.compact.join("\n")}\n"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def node_rules(hostname, record, index)
|
|
27
|
+
return if record['network_phase'] == 'bootstrap'
|
|
28
|
+
|
|
29
|
+
domains = HostPolicy.new(@config).resolve(hostname)
|
|
30
|
+
rules("node_#{index}", address(hostname), domains)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def address(name)
|
|
34
|
+
@resources[name]&.dig('networks', @network, 'IPAddress')
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def rules(name, address, domains, authorization = nil)
|
|
38
|
+
return unless address && !address.empty? && !domains.empty?
|
|
39
|
+
|
|
40
|
+
IPAddr.new(address)
|
|
41
|
+
entries = ["acl #{name}_source src #{address}"]
|
|
42
|
+
condition = "#{name}_source"
|
|
43
|
+
if authorization
|
|
44
|
+
entries << "acl #{name}_identity req_header Proxy-Authorization " \
|
|
45
|
+
"^Basic[[:space:]]#{Regexp.escape(authorization)}$"
|
|
46
|
+
condition += " #{name}_identity"
|
|
47
|
+
end
|
|
48
|
+
entries.concat(ProxyPolicy.domain_rules(domains, name: "#{name}_domains", condition: condition)).join("\n")
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|