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,161 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Node
|
|
5
|
+
# rubocop:disable-next Metrics/ModuleLength -- VM creation and its ordered first-boot preparation are one workflow.
|
|
6
|
+
module VMPreparation
|
|
7
|
+
def run(request)
|
|
8
|
+
@created_vm = nil
|
|
9
|
+
mutate do
|
|
10
|
+
server = ready_server
|
|
11
|
+
image, accelerator = preflight_vm(request)
|
|
12
|
+
record, overlay, seed = prepare_vm_disk(request, image, accelerator)
|
|
13
|
+
provision_vm(record, overlay, seed, server)
|
|
14
|
+
lifecycle_result(record.fetch('hostname'), :running, changed: true)
|
|
15
|
+
rescue StandardError => e
|
|
16
|
+
report_retained_vm(e)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def provision_vm(record, overlay, seed, server)
|
|
23
|
+
packages = package_bootstrap(record)
|
|
24
|
+
Network::Gateway.new(context: context, runtime: @runtime, state: @state).phase(record, bootstrap: true)
|
|
25
|
+
boot_vm(record, overlay, seed)
|
|
26
|
+
@bootstrap_proxy.start(@state, @requirements, source: record.fetch('peer').fetch('ip')) if
|
|
27
|
+
@requirements.required? || packages.required?
|
|
28
|
+
finish_vm(record, server, packages)
|
|
29
|
+
ensure
|
|
30
|
+
@bootstrap_proxy.cleanup(@state)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def report_retained_vm(error)
|
|
34
|
+
raise error unless @created_vm
|
|
35
|
+
|
|
36
|
+
name = @created_vm.fetch('hostname')
|
|
37
|
+
message = "#{error.message}\nVM #{name} retained for diagnosis. Cleanup: empeira node destroy #{name}"
|
|
38
|
+
raise error.exception(message), cause: error
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def prepare_vm_disk(request, image, accelerator)
|
|
42
|
+
@progress.stage(20, 'Preparing VM image...')
|
|
43
|
+
base = @cache.fetch(image)
|
|
44
|
+
record = reserve_vm(request, image.identity, accelerator)
|
|
45
|
+
@created_vm = record
|
|
46
|
+
@progress.stage(35, 'Creating overlay and cloud-init seed...')
|
|
47
|
+
overlay = @disk.create(hostname: record.fetch('hostname'), base: base,
|
|
48
|
+
size_gib: context.configuration.dig('vm', 'disk'))
|
|
49
|
+
[record, overlay, @cloud.prepare(record)]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def boot_vm(record, overlay, seed)
|
|
53
|
+
@progress.stage(50, 'Connecting VM to managed services...')
|
|
54
|
+
@peer.prepare(record, @state)
|
|
55
|
+
attachment = Network::Peer::Attachment.new(backend: @peer, record: record, state: @state)
|
|
56
|
+
refresh_dns
|
|
57
|
+
@progress.stage(55, 'Starting virtual machine...')
|
|
58
|
+
begin
|
|
59
|
+
record['pid'] = @qemu.launch(record: record, overlay: overlay, seed: seed, network: attachment)
|
|
60
|
+
ensure
|
|
61
|
+
save if record['pid']
|
|
62
|
+
end
|
|
63
|
+
@progress.stage(60, 'Waiting for VM network and bootstrap...')
|
|
64
|
+
verify_vm_boot(record)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def verify_vm_boot(record)
|
|
68
|
+
@guest.wait(record, progress: @progress)
|
|
69
|
+
::Empeira::VM::RootDisk.new(guest: @guest).verify!(record, size_gib: context.configuration.dig('vm', 'disk'))
|
|
70
|
+
VMBootstrap.new(guest: @guest).verify(record: record, bootstrap: Bootstrap.new(provider: 'vm'))
|
|
71
|
+
record['empeira_fact'] = ExternalFact.fingerprint('vm')
|
|
72
|
+
save
|
|
73
|
+
@cloud.finish(record, guest: @guest)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# rubocop:disable-next Metrics/AbcSize -- Verified certificate retirement precedes enrollment and ready-state persistence.
|
|
77
|
+
def finish_vm(record, server, packages)
|
|
78
|
+
@progress.stage(70, 'Installing/configuring Puppet agent...')
|
|
79
|
+
prepare_agent(record, packages)
|
|
80
|
+
reconcile_interfaces(record)
|
|
81
|
+
@progress.stage(85, 'Signing VM certificate...')
|
|
82
|
+
certificate_runtime = ::Empeira::VM::CertificateRuntime.new(runtime: @runtime, guest: @guest, record: record)
|
|
83
|
+
certificates = Certificates.new(runtime: certificate_runtime, server: server)
|
|
84
|
+
certificates.clean_retired(@state, record.fetch('hostname')) { save } if @state['retired_certificates']
|
|
85
|
+
certificates.enroll({ 'vm' => true }, record) { save }
|
|
86
|
+
reconcile_guest(record)
|
|
87
|
+
record['provisioned'] = true
|
|
88
|
+
record['state'] = 'running'
|
|
89
|
+
save
|
|
90
|
+
@progress.stage(95, 'Running Puppet...')
|
|
91
|
+
puppet_run(record)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# rubocop:disable-next Metrics/AbcSize, Metrics/MethodLength -- Preserve bootstrap cleanup before runtime activation.
|
|
95
|
+
def prepare_agent(record, packages)
|
|
96
|
+
execute = ->(arguments) { @guest.run(record, arguments, timeout: 300) }
|
|
97
|
+
package_configuration(record, execute).preserve do
|
|
98
|
+
if packages.required?
|
|
99
|
+
@progress.stage(75, 'Bootstrapping packages...')
|
|
100
|
+
packages.run(proxy_url: @bootstrap_proxy.url)
|
|
101
|
+
end
|
|
102
|
+
@agent.ensure_installed(record, requirements: @requirements, proxy_url: @bootstrap_proxy.url)
|
|
103
|
+
end
|
|
104
|
+
PackageSources.verify!(execute: ->(arguments) { @guest.run(record, arguments) })
|
|
105
|
+
@bootstrap_proxy.cleanup(@state)
|
|
106
|
+
record['network_phase'] = 'runtime'
|
|
107
|
+
save
|
|
108
|
+
Network::Gateway.new(context: context, runtime: @runtime, state: @state).phase(record, bootstrap: false)
|
|
109
|
+
refresh_dns
|
|
110
|
+
reconcile_runtime_proxy(record)
|
|
111
|
+
reconcile_interactive_tools(record)
|
|
112
|
+
configure_agent(record)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def package_configuration(record, execute)
|
|
116
|
+
family = PackageBootstrap::FAMILIES.fetch(record.fetch('os'))
|
|
117
|
+
klass = family == 'debian' ? AptConfiguration : RpmConfiguration
|
|
118
|
+
options = family == 'debian' ? { on_retry: @progress.method(:warning) } : {}
|
|
119
|
+
klass.new(os: record.fetch('os'), execute: execute, **options)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def package_bootstrap(record)
|
|
123
|
+
PackageBootstrap.new(config: context.configuration.dig('bootstrap', 'packages'), os: record.fetch('os'),
|
|
124
|
+
execute: ->(arguments) { @guest.run(record, arguments, timeout: 300) },
|
|
125
|
+
copy: lambda { |source, destination, mode|
|
|
126
|
+
@guest.copy_to(record, source, destination, mode: mode)
|
|
127
|
+
},
|
|
128
|
+
rpm_options: @requirements.rpm_options,
|
|
129
|
+
progress: @progress.method(:heartbeat))
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def refresh_dns
|
|
133
|
+
plan = ControlPlane::Plan.new(context: context)
|
|
134
|
+
ControlPlane::Discovery.new(plan: plan, runtime: @runtime, state: @state).refresh
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def configure_agent(record)
|
|
138
|
+
{ 'certname' => record.fetch('hostname'), 'server' => 'server.empeira.internal',
|
|
139
|
+
'environment' => context.configuration.dig('server', 'environment') }.each do |key, value|
|
|
140
|
+
result = @guest.run(record, [Certificates::PUPPET, 'config', 'set', key, value, '--section', 'main'])
|
|
141
|
+
next if result.success?
|
|
142
|
+
|
|
143
|
+
details = Execution::Diagnostics.command(result, operation: "VM agent configuration: #{key}", tool: 'puppet')
|
|
144
|
+
raise Error, "VM agent configuration failed; node retained for diagnosis\n#{details}", cause: nil
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def puppet_run(record)
|
|
149
|
+
reconcile_provider_fact(record)
|
|
150
|
+
result = @progress.streaming do
|
|
151
|
+
@guest.stream(record, RuntimeProxy.command(context, PuppetCommand.arguments))
|
|
152
|
+
end
|
|
153
|
+
record['last_puppet_exit'] = result.exit_status
|
|
154
|
+
save
|
|
155
|
+
return result if [0, 2].include?(result.exit_status)
|
|
156
|
+
|
|
157
|
+
raise Error, "Puppet agent failed (exit #{result.exit_status}); VM retained for shell/logs"
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'socket'
|
|
5
|
+
|
|
6
|
+
module Empeira
|
|
7
|
+
module Node
|
|
8
|
+
# VM records live in the same locked workspace inventory as container records.
|
|
9
|
+
# rubocop:disable-next Metrics/ModuleLength -- Workspace state and record validation share one boundary.
|
|
10
|
+
module VMState
|
|
11
|
+
def list
|
|
12
|
+
load_state
|
|
13
|
+
health = @peer.health(vm_records.values)
|
|
14
|
+
vm_records.map do |_, record|
|
|
15
|
+
record.slice('hostname', 'provider', 'os', 'version', 'last_puppet_exit').merge(
|
|
16
|
+
'runtime' => 'qemu', 'state' => observed_state(record),
|
|
17
|
+
'network' => if health[record.fetch('hostname')]
|
|
18
|
+
'running'
|
|
19
|
+
else
|
|
20
|
+
(record['state'] == 'stopped' ? 'reserved' : 'degraded')
|
|
21
|
+
end
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def inspect_resource(name:)
|
|
27
|
+
load_state
|
|
28
|
+
record = vm_records[name.downcase]
|
|
29
|
+
return unless record
|
|
30
|
+
|
|
31
|
+
state = record['provisioned'] && @qemu.observed_running?(record) ? :running : :stopped
|
|
32
|
+
Providers::Resource.new(name: record.fetch('hostname'), owner: context.workspace.id, state: state)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def observed_state(record)
|
|
38
|
+
return 'incomplete' unless record['provisioned']
|
|
39
|
+
|
|
40
|
+
return 'running' if @qemu.observed_running?(record)
|
|
41
|
+
|
|
42
|
+
return 'preparing' if record['state'] == 'preparing'
|
|
43
|
+
|
|
44
|
+
record['state'] == 'stopped' ? 'stopped' : 'missing'
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def vm_records
|
|
48
|
+
@nodes.select { |_, record| record['provider'] == 'vm' }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def mutate(availability: true)
|
|
52
|
+
@build_info.require_compatible!(context.configuration.dig('requirements', 'empeira'))
|
|
53
|
+
@store.with_lock do
|
|
54
|
+
load_state
|
|
55
|
+
check_runtime!(availability: availability)
|
|
56
|
+
yield
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def load_state(state = @store.load)
|
|
61
|
+
@state = state
|
|
62
|
+
@nodes = state&.fetch('nodes', {}) || {}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def check_runtime!(availability: true)
|
|
66
|
+
raise Error, 'Control plane is down; run empeira up first' unless @state&.dig('resources', 'network', 'id')
|
|
67
|
+
if @state.fetch('runtime') != context.container_engine
|
|
68
|
+
raise Error, 'Node runtime differs from workspace ownership; restore runtime.container_engine'
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
@runtime.check_available! if availability
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def save
|
|
75
|
+
@state['nodes'] = @nodes
|
|
76
|
+
@store.write(@state)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def fetch_vm(name)
|
|
80
|
+
record = vm_records[name.downcase]
|
|
81
|
+
raise Providers::NotFound, 'VM node does not exist in this workspace' unless record
|
|
82
|
+
|
|
83
|
+
record
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def ready_server
|
|
87
|
+
plan = ControlPlane::Plan.new(context: context)
|
|
88
|
+
definition = plan.definitions.fetch('server')
|
|
89
|
+
expected = @state.dig('control_plane', 'services', 'server', 'id')
|
|
90
|
+
server = @runtime.inspect_service(definition, expected_id: expected)
|
|
91
|
+
unless server && server['state'] == 'running' &&
|
|
92
|
+
server.dig('labels', 'io.empeira.definition') == definition.fingerprint &&
|
|
93
|
+
ControlPlane::Health.new(runtime: @runtime, plan: plan).ready?('server', 'server' => server)
|
|
94
|
+
raise Error, 'Configuration server is not ready; run empeira up first'
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
server
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def reserve_vm(request, identity, accelerator)
|
|
101
|
+
name = request.hostname.downcase
|
|
102
|
+
reject_vm_name!(name)
|
|
103
|
+
record = vm_record(request, identity, accelerator, name)
|
|
104
|
+
@nodes[name] = record
|
|
105
|
+
save
|
|
106
|
+
record
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def reject_vm_name!(name)
|
|
110
|
+
aliases = [name, name.delete_suffix('.empeira.internal')]
|
|
111
|
+
taken = @nodes.keys.any? { |key| aliases.include?(key.delete_suffix('.empeira.internal')) }
|
|
112
|
+
return unless taken || aliases.intersect?(Network::Naming.new.reserved(context.configuration))
|
|
113
|
+
|
|
114
|
+
raise Providers::AlreadyExists, 'Hostname is reserved or already exists in this workspace'
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def vm_record(request, identity, accelerator, name)
|
|
118
|
+
request.to_h.transform_keys(&:to_s).merge(
|
|
119
|
+
'hostname' => name, 'engine' => 'qemu', 'accelerator' => accelerator,
|
|
120
|
+
'architecture' => context.platform.architecture.to_s, 'base_image' => identity.to_h,
|
|
121
|
+
'overlay' => "vms/#{name}/disk.qcow2", 'network' => @state.dig('resources', 'network', 'logical_identity'),
|
|
122
|
+
'mac_address' => vm_mac(name),
|
|
123
|
+
'peer' => peer_lease,
|
|
124
|
+
'management_layout' => ::Empeira::VM::Management::VERSION, 'pid' => nil,
|
|
125
|
+
'network_phase' => 'bootstrap', 'state' => 'preparing', 'provisioned' => false,
|
|
126
|
+
'certificate_key' => nil, 'last_puppet_exit' => nil,
|
|
127
|
+
'created_at' => Time.now.utc.iso8601
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# rubocop:disable-next Metrics/AbcSize -- Lease and shared DNS/gateway bindings are one identity.
|
|
132
|
+
def peer_lease
|
|
133
|
+
plan = ControlPlane::Plan.new(context: context)
|
|
134
|
+
dns = @runtime.inspect_service(plan.definitions.fetch('dns'),
|
|
135
|
+
expected_id: @state.dig('control_plane', 'services', 'dns', 'id'))
|
|
136
|
+
address = dns&.dig('networks', plan.network, 'IPAddress')
|
|
137
|
+
raise Error, 'CoreDNS is unavailable on the peer network' unless address
|
|
138
|
+
|
|
139
|
+
Network::Peer::Layout.new(@state.fetch('peer_network').fetch('subnet')).lease(@nodes)
|
|
140
|
+
.merge('backend' => @peer.key, 'dns' => address,
|
|
141
|
+
'gateway' => Network::Gateway.address(@state.fetch('peer_network').fetch('subnet')))
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def vm_mac(_hostname)
|
|
145
|
+
hex = SecureRandom.hex(4)
|
|
146
|
+
"52:54:#{hex.scan(/../).join(':')}"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def base_for(record)
|
|
150
|
+
fields = record.fetch('base_image').transform_keys(&:to_sym)
|
|
151
|
+
identity = Images::Identity.new(**fields)
|
|
152
|
+
context.locations.image(identity).join('base.qcow2')
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def lifecycle_result(name, state, changed:)
|
|
156
|
+
resource = Providers::Resource.new(name: name, owner: context.workspace.id, state: state)
|
|
157
|
+
Providers::Result.new(resource: resource, changed: changed)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rbconfig'
|
|
4
|
+
|
|
5
|
+
module Empeira
|
|
6
|
+
module Platform
|
|
7
|
+
class Facts
|
|
8
|
+
attr_reader :os, :architecture
|
|
9
|
+
|
|
10
|
+
def initialize(host_os: RbConfig::CONFIG.fetch('host_os'), host_cpu: RbConfig::CONFIG.fetch('host_cpu'),
|
|
11
|
+
environment: ENV, kernel_release: nil)
|
|
12
|
+
@os = detect_os(host_os, environment, kernel_release)
|
|
13
|
+
@architecture = normalize_architecture(host_cpu)
|
|
14
|
+
freeze
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def process_groups?
|
|
18
|
+
true
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def vm_install_hint
|
|
22
|
+
return 'Install QEMU, xorriso and OpenSSH, for example: brew install qemu xorriso' if os == :macos
|
|
23
|
+
|
|
24
|
+
release = File.read('/etc/os-release')
|
|
25
|
+
if release.match?(/^(?:ID|ID_LIKE)=.*(?:ubuntu|debian)/)
|
|
26
|
+
package = architecture == :arm64 ? 'qemu-system-arm qemu-efi-aarch64' : 'qemu-system-x86'
|
|
27
|
+
return "Install missing dependencies: sudo apt install #{package} qemu-utils xorriso openssh-client"
|
|
28
|
+
end
|
|
29
|
+
'Install QEMU system tools, qemu-img, xorriso and OpenSSH using your host distribution package manager.'
|
|
30
|
+
rescue SystemCallError
|
|
31
|
+
'Install QEMU system tools, qemu-img, xorriso and OpenSSH using your host distribution package manager.'
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def detect_os(host_os, environment, kernel_release)
|
|
37
|
+
case host_os.downcase
|
|
38
|
+
when /linux/ then wsl?(environment, kernel_release) ? :wsl : :linux
|
|
39
|
+
when /darwin/ then :macos
|
|
40
|
+
else raise UnsupportedPlatform, 'Unsupported operating system (expected Linux, macOS, or WSL2)'
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def wsl?(environment, kernel_release)
|
|
45
|
+
return true if %w[WSL_INTEROP WSL_DISTRO_NAME].any? { |key| !environment[key].to_s.empty? }
|
|
46
|
+
|
|
47
|
+
release = kernel_release || read_kernel_release
|
|
48
|
+
release.match?(/microsoft|wsl/i)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def read_kernel_release
|
|
52
|
+
File.read('/proc/sys/kernel/osrelease')
|
|
53
|
+
rescue SystemCallError
|
|
54
|
+
''
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def normalize_architecture(cpu)
|
|
58
|
+
case cpu.downcase
|
|
59
|
+
when /\A(?:x86_64|amd64|x64)(?:\W|\z)/ then :amd64
|
|
60
|
+
when /\A(?:aarch64|arm64)(?:\W|\z)/ then :arm64
|
|
61
|
+
else raise UnsupportedPlatform, 'Unsupported architecture (expected amd64 or arm64)'
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'etc'
|
|
4
|
+
require 'shellwords'
|
|
5
|
+
|
|
6
|
+
module Empeira
|
|
7
|
+
module Platform
|
|
8
|
+
class Kvm
|
|
9
|
+
DEVICE = '/dev/kvm'
|
|
10
|
+
|
|
11
|
+
def initialize(platform:)
|
|
12
|
+
@platform = platform
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def verify!
|
|
16
|
+
version = File.open(DEVICE, File::RDWR) do |device|
|
|
17
|
+
device.ioctl(0xae00)
|
|
18
|
+
rescue SystemCallError => e
|
|
19
|
+
raise UnavailableFeature, "KVM device opens but KVM_GET_API_VERSION ioctl fails (#{e.class})", cause: nil
|
|
20
|
+
end
|
|
21
|
+
return version if version == 12
|
|
22
|
+
|
|
23
|
+
raise UnavailableFeature, "KVM returned unsupported API version #{version} (expected 12)"
|
|
24
|
+
rescue Errno::ENOENT
|
|
25
|
+
raise UnavailableFeature, 'KVM is unavailable: /dev/kvm is missing; enable host KVM virtualization', cause: nil
|
|
26
|
+
rescue Errno::EACCES, Errno::EPERM
|
|
27
|
+
raise UnavailableFeature, permission_diagnostic, cause: nil
|
|
28
|
+
rescue SystemCallError => e
|
|
29
|
+
raise UnavailableFeature, "KVM device cannot be opened (#{e.class}); check host device policy", cause: nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def permission_diagnostic
|
|
35
|
+
stat = File.stat(DEVICE)
|
|
36
|
+
groups = current_groups
|
|
37
|
+
["KVM is unavailable: #{DEVICE} is #{access_description}.",
|
|
38
|
+
"Owner: #{user(stat.uid)}:#{group(stat.gid)} (mode #{format('%04o', stat.mode & 0o777)})",
|
|
39
|
+
"Current user: #{user(Process.euid)}", "Current groups: #{group_names(groups)}",
|
|
40
|
+
permission_hint(stat, groups)].join("\n")
|
|
41
|
+
rescue SystemCallError
|
|
42
|
+
'KVM is unavailable: device permissions deny access; inspect /dev/kvm ownership and host device policy'
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def group_names(groups)
|
|
46
|
+
groups.map { |id| group(id) }.join(' ')
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def current_groups
|
|
50
|
+
(Process.groups + [Process.egid]).uniq
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def access_description
|
|
54
|
+
access = []
|
|
55
|
+
access << 'unreadable' unless File.readable?(DEVICE)
|
|
56
|
+
access << 'unwritable' unless File.writable?(DEVICE)
|
|
57
|
+
access << 'denied by device policy' if access.empty?
|
|
58
|
+
access.join(' and ')
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def missing_group?(stat, groups)
|
|
62
|
+
Process.euid != stat.uid && !groups.include?(stat.gid) && stat.mode.allbits?(0o060) &&
|
|
63
|
+
(!File.readable?(DEVICE) || !File.writable?(DEVICE))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def permission_hint(stat, groups)
|
|
67
|
+
unless missing_group?(stat, groups)
|
|
68
|
+
return 'Device permissions or host security policy deny access; check mode, ACLs and device restrictions.'
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
required_group = Shellwords.escape(group(stat.gid))
|
|
72
|
+
hint = "Required group: #{group(stat.gid)}\nSuggested fix: sudo usermod -aG #{required_group} \"$USER\"\n" \
|
|
73
|
+
'Start a new login session for group membership to take effect.'
|
|
74
|
+
hint += "\nOn WSL2 restart the instance/session: wsl.exe --shutdown" if @platform.os == :wsl
|
|
75
|
+
hint
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def user(id)
|
|
79
|
+
Etc.getpwuid(id).name
|
|
80
|
+
rescue ArgumentError
|
|
81
|
+
id.to_s
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def group(id)
|
|
85
|
+
Etc.getgrgid(id).name
|
|
86
|
+
rescue ArgumentError
|
|
87
|
+
id.to_s
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pathname'
|
|
4
|
+
require 'tmpdir'
|
|
5
|
+
|
|
6
|
+
module Empeira
|
|
7
|
+
module Platform
|
|
8
|
+
class Locations
|
|
9
|
+
attr_reader :cache, :state, :temporary, :home
|
|
10
|
+
|
|
11
|
+
def initialize(facts: Facts.new, home: Dir.home, environment: ENV, temporary_root: Dir.tmpdir)
|
|
12
|
+
@home = Pathname(home)
|
|
13
|
+
@environment = environment
|
|
14
|
+
@cache, @state = roots(facts.os).map { |path| path.join('empeira') }
|
|
15
|
+
@temporary = Pathname(temporary_root)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def workspace(identity)
|
|
19
|
+
state.join('workspaces', identity.id)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def user_configuration
|
|
23
|
+
home.join('.empeira.yaml')
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def addons
|
|
27
|
+
home.join('.empeira', 'addons').expand_path
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def image(identity)
|
|
31
|
+
cache.join('images', identity.cache_key)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def roots(os)
|
|
37
|
+
case os
|
|
38
|
+
when :macos then [@home.join('Library/Caches'), @home.join('Library/Application Support')]
|
|
39
|
+
else
|
|
40
|
+
[absolute_environment('XDG_CACHE_HOME', @home.join('.cache')),
|
|
41
|
+
absolute_environment('XDG_STATE_HOME', @home.join('.local/state'))]
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def absolute_environment(key, fallback)
|
|
46
|
+
value = @environment[key]
|
|
47
|
+
value && Pathname(value).absolute? ? Pathname(value) : fallback
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Platform
|
|
5
|
+
class Resolvers
|
|
6
|
+
attr_reader :routes
|
|
7
|
+
|
|
8
|
+
def initialize(platform:, runner:, reader: File)
|
|
9
|
+
@platform = platform
|
|
10
|
+
@runner = runner
|
|
11
|
+
@reader = reader
|
|
12
|
+
@routes = {}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def resolve(config)
|
|
16
|
+
return usable(config.fetch('servers')) if config.fetch('mode') == 'explicit'
|
|
17
|
+
|
|
18
|
+
usable(@platform.os == :macos ? macos : linux)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def linux
|
|
24
|
+
addresses = read('/etc/resolv.conf').scan(/^nameserver\s+(\S+)/).flatten
|
|
25
|
+
return addresses if addresses.any? { |address| reachable?(address) }
|
|
26
|
+
|
|
27
|
+
read('/run/systemd/resolve/resolv.conf').scan(/^nameserver\s+(\S+)/).flatten
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def macos
|
|
31
|
+
result = @runner.run('scutil', arguments: ['--dns'], timeout: 10)
|
|
32
|
+
unless result.success?
|
|
33
|
+
raise Error,
|
|
34
|
+
'Cannot discover macOS DNS; configure dns.upstream.mode=explicit and servers'
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
parse_macos(result.stdout)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def parse_macos(output)
|
|
41
|
+
defaults = []
|
|
42
|
+
output.split(/^resolver #/).each do |block|
|
|
43
|
+
next if block.match?(/options\s*:\s*mdns/)
|
|
44
|
+
|
|
45
|
+
addresses = macos_addresses(block)
|
|
46
|
+
domain = block[/^\s*domain\s*:\s*([a-zA-Z0-9.-]+)\s*$/, 1]&.downcase
|
|
47
|
+
defaults = route_or_default(domain, addresses, defaults)
|
|
48
|
+
end
|
|
49
|
+
defaults
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def route_or_default(domain, addresses, defaults)
|
|
53
|
+
return defaults if domain&.end_with?('empeira.internal')
|
|
54
|
+
return defaults.empty? ? addresses : defaults unless domain
|
|
55
|
+
|
|
56
|
+
@routes[domain] = addresses unless addresses.empty?
|
|
57
|
+
defaults
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def macos_addresses(block)
|
|
61
|
+
block.scan(/nameserver\[\d+\]\s*:\s*(\S+)/).flatten.select { |ip| reachable?(ip) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def read(path)
|
|
65
|
+
@reader.read(path)
|
|
66
|
+
rescue SystemCallError
|
|
67
|
+
''
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def reachable?(address)
|
|
71
|
+
ip = IPAddr.new(address)
|
|
72
|
+
!ip.loopback? && !ip.link_local? && ip.to_i.positive?
|
|
73
|
+
rescue IPAddr::InvalidAddressError
|
|
74
|
+
false
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def usable(addresses)
|
|
78
|
+
result = addresses.select { |address| reachable?(address) }.uniq
|
|
79
|
+
return result unless result.empty?
|
|
80
|
+
|
|
81
|
+
raise Error, 'No container-reachable DNS upstream found; configure dns.upstream.mode=explicit and servers'
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Platform
|
|
5
|
+
class Routes
|
|
6
|
+
def initialize(platform:, runner:)
|
|
7
|
+
@platform = platform
|
|
8
|
+
@runner = runner
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def ipv4
|
|
12
|
+
return linux_routes if @platform.os != :macos
|
|
13
|
+
|
|
14
|
+
mac_routes
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def mac_routes
|
|
20
|
+
result = @runner.run('netstat', arguments: %w[-rn -f inet], timeout: 10)
|
|
21
|
+
raise Error, 'Cannot inspect host IPv4 routes before allocating the peer network' unless result.success?
|
|
22
|
+
|
|
23
|
+
result.stdout.lines.filter_map do |line|
|
|
24
|
+
destination = line.split.first
|
|
25
|
+
next unless destination&.match?(/\A\d/)
|
|
26
|
+
|
|
27
|
+
expand_destination(destination)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def expand_destination(destination)
|
|
32
|
+
address, prefix = destination.split('/')
|
|
33
|
+
octets = address.split('.')
|
|
34
|
+
"#{(octets + (['0'] * (4 - octets.size))).join('.')}/#{prefix || (octets.size * 8)}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def linux_routes
|
|
38
|
+
result = @runner.run('ip', arguments: %w[-j -4 route show table all], timeout: 10)
|
|
39
|
+
raise Error, 'iproute2 is required to verify host routes before peer subnet allocation' unless result.success?
|
|
40
|
+
|
|
41
|
+
JSON.parse(result.stdout).filter_map do |route|
|
|
42
|
+
destination = route.fetch('dst')
|
|
43
|
+
destination unless %w[default 0.0.0.0/0].include?(destination)
|
|
44
|
+
end
|
|
45
|
+
rescue JSON::ParserError, KeyError
|
|
46
|
+
raise Error, 'Cannot verify host IPv4 route inventory', cause: nil
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|