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,404 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'socket'
|
|
6
|
+
require 'timeout'
|
|
7
|
+
|
|
8
|
+
module Empeira
|
|
9
|
+
module VM
|
|
10
|
+
# QEMU process control is a replaceable VM-engine concern.
|
|
11
|
+
# rubocop:disable-next Metrics/ClassLength -- Launch and ownership-safe monitor control form one engine boundary.
|
|
12
|
+
class QemuRuntime
|
|
13
|
+
def initialize(engine:, runner:, context:)
|
|
14
|
+
@engine = engine
|
|
15
|
+
@runner = runner
|
|
16
|
+
@context = context
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def launch(record:, overlay:, seed:, network:)
|
|
20
|
+
Management.validate!(record)
|
|
21
|
+
hostname = record.fetch('hostname')
|
|
22
|
+
monitor = monitor_path(hostname)
|
|
23
|
+
ensure_launchable_monitor!(record, monitor)
|
|
24
|
+
execute_launch(record, overlay, seed, network, monitor)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def running?(record)
|
|
28
|
+
monitor = monitor_path(record.fetch('hostname'))
|
|
29
|
+
return false unless monitor.socket?
|
|
30
|
+
|
|
31
|
+
Timeout.timeout(2) do
|
|
32
|
+
UNIXSocket.open(monitor) do |socket|
|
|
33
|
+
read_monitor_prompt(socket)
|
|
34
|
+
verify_monitor_identity!(socket, record)
|
|
35
|
+
true
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
rescue SystemCallError, IOError, Timeout::Error
|
|
39
|
+
false
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def observed_running?(record)
|
|
43
|
+
process_alive?(record) && monitor_path(record.fetch('hostname')).socket?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def stop(record)
|
|
47
|
+
unless running?(record)
|
|
48
|
+
raise Error, 'QEMU process is alive but monitor is unavailable; VM disk retained' if process_alive?(record)
|
|
49
|
+
|
|
50
|
+
return nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
monitor_command(record, 'system_powerdown')
|
|
54
|
+
wait_for_shutdown(record)
|
|
55
|
+
if running?(record)
|
|
56
|
+
monitor_command(record, 'quit')
|
|
57
|
+
wait_for_shutdown(record, seconds: 5)
|
|
58
|
+
end
|
|
59
|
+
raise Error, 'QEMU did not stop; VM overlay retained' if running?(record)
|
|
60
|
+
|
|
61
|
+
File.unlink(monitor_path(record.fetch('hostname'))) if monitor_path(record.fetch('hostname')).exist?
|
|
62
|
+
record
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def cleanup(record)
|
|
66
|
+
raise Error, 'QEMU process is still alive; monitor state retained' if process_alive?(record)
|
|
67
|
+
|
|
68
|
+
directory = monitor_directory(record.fetch('hostname'))
|
|
69
|
+
return unless directory.exist? || directory.symlink?
|
|
70
|
+
|
|
71
|
+
validate_monitor_directory!(directory)
|
|
72
|
+
validate_management_cleanup!(record)
|
|
73
|
+
unlink_monitor_socket(directory.join('monitor.sock'))
|
|
74
|
+
unlink_monitor_socket(directory.join('console.sock'))
|
|
75
|
+
unlink_management_socket(record)
|
|
76
|
+
Dir.rmdir(directory)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def process_alive?(record)
|
|
80
|
+
pid = record['pid'] || unrecorded_pid(record)
|
|
81
|
+
return false unless pid.is_a?(Integer) && pid.positive?
|
|
82
|
+
|
|
83
|
+
Process.kill(0, pid)
|
|
84
|
+
true
|
|
85
|
+
rescue Errno::ESRCH
|
|
86
|
+
false
|
|
87
|
+
rescue Errno::EPERM
|
|
88
|
+
true
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def unrecorded_pid(record)
|
|
92
|
+
path = pid_path(record.fetch('hostname'))
|
|
93
|
+
return unless path.exist? || path.symlink?
|
|
94
|
+
|
|
95
|
+
root = @context.locations.workspace(@context.workspace)
|
|
96
|
+
Infrastructure::OwnedFiles.verify_removable!(path, root: root)
|
|
97
|
+
value = File.read(path, 32).strip
|
|
98
|
+
unless value.match?(/\A[1-9][0-9]{0,9}\z/)
|
|
99
|
+
raise Error, 'QEMU PID file is not safely interpretable; VM disk retained'
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
Integer(value, 10)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def serial_log(hostname)
|
|
106
|
+
node_directory(hostname).join('serial.log')
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def console(record)
|
|
110
|
+
raise Error, 'VM is stopped; start it before attaching the console' unless running?(record)
|
|
111
|
+
|
|
112
|
+
path = console_path(record.fetch('hostname'))
|
|
113
|
+
validate_monitor_directory!(path.dirname)
|
|
114
|
+
metadata = path.lstat
|
|
115
|
+
unless metadata.socket? && metadata.uid == Process.uid
|
|
116
|
+
raise Providers::OwnershipError, 'VM console endpoint ownership is invalid'
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
@runner.console(path, guidance: console_guidance(record.fetch('hostname')))
|
|
120
|
+
rescue Errno::ENOENT
|
|
121
|
+
raise Error, 'VM console endpoint is missing'
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def console_path(hostname)
|
|
125
|
+
monitor_directory(hostname).join('console.sock')
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def management_socket(record)
|
|
129
|
+
Management.validate!(record)
|
|
130
|
+
raise Error, 'VM is stopped; management unavailable' unless running?(record)
|
|
131
|
+
|
|
132
|
+
path = management_path(record.fetch('hostname'))
|
|
133
|
+
verify_management_socket!(record, path)
|
|
134
|
+
path
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def with_management_socket(record)
|
|
138
|
+
Management.validate!(record)
|
|
139
|
+
raise Error, 'VM is stopped; management unavailable' unless running?(record)
|
|
140
|
+
|
|
141
|
+
Management::SLOTS.times do |slot|
|
|
142
|
+
path = management_path(record.fetch('hostname'), slot)
|
|
143
|
+
expected = slot.zero? ? record['management_socket'] : record.dig('management_sockets', slot.to_s)
|
|
144
|
+
verify_management_socket!(record, path, expected: expected)
|
|
145
|
+
lock = management_slot_lock(path)
|
|
146
|
+
next unless lock
|
|
147
|
+
|
|
148
|
+
begin
|
|
149
|
+
return yield path, expected
|
|
150
|
+
ensure
|
|
151
|
+
lock.close
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
raise Error, 'All eight VM management sessions are busy; detach a session and retry'
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
private
|
|
158
|
+
|
|
159
|
+
def console_guidance(hostname)
|
|
160
|
+
message = "Connected to VM serial console.\nPress Ctrl+] to detach. The VM will keep running.\n"
|
|
161
|
+
return message if @context.configuration.dig('vm', 'console', 'root_password')
|
|
162
|
+
|
|
163
|
+
"#{message}No console password is configured. Use empeira node ssh #{hostname} instead.\n"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def validate_monitor_directory!(directory)
|
|
167
|
+
metadata = File.lstat(directory)
|
|
168
|
+
return if metadata.directory? && metadata.uid == Process.uid && metadata.mode.nobits?(0o077)
|
|
169
|
+
|
|
170
|
+
raise Error, 'VM monitor directory ownership changed; cleanup refused'
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def unlink_monitor_socket(socket)
|
|
174
|
+
return unless socket.exist? || socket.symlink?
|
|
175
|
+
|
|
176
|
+
if socket.symlink? || !socket.lstat.socket? || socket.lstat.uid != Process.uid
|
|
177
|
+
raise Error, 'Unexpected VM monitor path; cleanup refused'
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
File.unlink(socket) if socket.socket?
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def ensure_launchable_monitor!(record, monitor)
|
|
184
|
+
prepare_monitor_directory(record.fetch('hostname'))
|
|
185
|
+
raise Error, 'Recorded QEMU process is still alive; refusing a second launch' if process_alive?(record)
|
|
186
|
+
|
|
187
|
+
unlink_management_socket(record)
|
|
188
|
+
unlink_monitor_socket(console_path(record.fetch('hostname')))
|
|
189
|
+
File.unlink(monitor) if monitor.socket? && !running?(record)
|
|
190
|
+
raise Error, 'VM monitor already exists; inspect the recorded process first' if monitor.exist?
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def execute_launch(record, overlay, seed, network, monitor)
|
|
194
|
+
arguments = launch_arguments(record, overlay, seed, network.arguments, monitor)
|
|
195
|
+
network.launch(@engine.executable, arguments)
|
|
196
|
+
|
|
197
|
+
pid = Integer(File.read(pid_path(record.fetch('hostname'))).strip)
|
|
198
|
+
record['pid'] = pid
|
|
199
|
+
raise Error, 'QEMU did not create its owned monitor socket' unless running?(record)
|
|
200
|
+
|
|
201
|
+
capture_management_socket!(record)
|
|
202
|
+
network.connect
|
|
203
|
+
|
|
204
|
+
pid
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def wait_for_shutdown(record, seconds: 30)
|
|
208
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + seconds
|
|
209
|
+
while (running?(record) || process_alive?(record)) && Process.clock_gettime(Process::CLOCK_MONOTONIC) < deadline
|
|
210
|
+
sleep 0.25
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def launch_arguments(record, overlay, seed, network, monitor)
|
|
215
|
+
hostname = record.fetch('hostname')
|
|
216
|
+
arch = @context.platform.architecture
|
|
217
|
+
machine = arch == :arm64 ? 'virt' : 'q35'
|
|
218
|
+
['-name', process_name(record), '-machine', machine,
|
|
219
|
+
'-accel', @engine.accelerator, '-cpu', 'host', '-m', record.fetch('memory').to_s,
|
|
220
|
+
'-smp', record.fetch('cpus').to_s, '-display', 'none', '-daemonize',
|
|
221
|
+
'-chardev', console_device(hostname), '-serial', 'chardev:console',
|
|
222
|
+
'-pidfile', pid_path(hostname).to_s,
|
|
223
|
+
'-monitor', "unix:#{escape(monitor)},server=on,wait=off",
|
|
224
|
+
*management_devices(hostname), *boot_drives(overlay, seed), *firmware(arch), *network]
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def management_devices(hostname)
|
|
228
|
+
['-device', 'virtio-serial-pci,id=management', *Management::SLOTS.times.flat_map do |slot|
|
|
229
|
+
identifier = slot.zero? ? 'management' : "management#{slot}"
|
|
230
|
+
['-chardev', "socket,id=#{identifier},path=#{escape(management_path(hostname, slot))},server=on,wait=off",
|
|
231
|
+
'-device', "virtserialport,chardev=#{identifier},name=org.empeira.management.#{slot}"]
|
|
232
|
+
end]
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def management_path(hostname, slot = 0)
|
|
236
|
+
monitor_directory(hostname).join(slot.zero? ? 'guest.sock' : "guest-#{slot}.sock")
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# rubocop:disable-next Metrics/AbcSize -- Pin each private VirtIO endpoint before exposing management.
|
|
240
|
+
def capture_management_socket!(record)
|
|
241
|
+
record['management_sockets'] = Management::SLOTS.times.to_h do |slot|
|
|
242
|
+
path = management_path(record.fetch('hostname'), slot)
|
|
243
|
+
validate_monitor_directory!(path.dirname)
|
|
244
|
+
metadata = path.lstat
|
|
245
|
+
unless metadata.socket? && metadata.uid == Process.uid
|
|
246
|
+
raise Providers::OwnershipError, 'QEMU management endpoint is unsafe; VM retained'
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
File.chmod(0o600, path)
|
|
250
|
+
[slot.to_s, { 'device' => metadata.dev, 'inode' => metadata.ino }]
|
|
251
|
+
end
|
|
252
|
+
record['management_socket'] = record['management_sockets'].fetch('0')
|
|
253
|
+
verify_management_socket!(record, management_path(record.fetch('hostname')))
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def verify_management_socket!(record, path, expected: record['management_socket'])
|
|
257
|
+
validate_monitor_directory!(path.dirname)
|
|
258
|
+
metadata = path.lstat
|
|
259
|
+
unless metadata.socket? && metadata.uid == Process.uid && metadata.mode.nobits?(0o077) &&
|
|
260
|
+
expected == { 'device' => metadata.dev, 'inode' => metadata.ino }
|
|
261
|
+
raise Providers::OwnershipError,
|
|
262
|
+
'VM management socket ownership or recorded identity changed; operation refused'
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def validate_management_cleanup!(record)
|
|
267
|
+
path = management_path(record.fetch('hostname'))
|
|
268
|
+
verify_management_socket!(record, path) if path.exist? || path.symlink?
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def unlink_management_socket(record)
|
|
272
|
+
Management::SLOTS.times do |slot|
|
|
273
|
+
path = management_path(record.fetch('hostname'), slot)
|
|
274
|
+
if path.exist? || path.symlink?
|
|
275
|
+
expected = slot.zero? ? record['management_socket'] : record.dig('management_sockets', slot.to_s)
|
|
276
|
+
verify_management_socket!(record, path, expected: expected)
|
|
277
|
+
File.unlink(path)
|
|
278
|
+
end
|
|
279
|
+
lock = Pathname("#{path}.lock")
|
|
280
|
+
unlink_management_lock(lock) if lock.exist? || lock.symlink?
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity -- Ownership and nonblocking acquisition precede transferring the live descriptor.
|
|
285
|
+
def management_slot_lock(path)
|
|
286
|
+
target = Pathname("#{path}.lock")
|
|
287
|
+
raise Providers::OwnershipError, 'VM management session lock is a symlink' if target.symlink?
|
|
288
|
+
|
|
289
|
+
# rubocop:disable-next Style/FileOpen -- The caller owns and closes the acquired slot descriptor.
|
|
290
|
+
lock = File.open(target, File::RDWR | File::CREAT | File::NOFOLLOW, 0o600)
|
|
291
|
+
metadata = lock.stat
|
|
292
|
+
unless metadata.file? && metadata.uid == Process.uid && metadata.mode.nobits?(0o077)
|
|
293
|
+
raise Providers::OwnershipError, 'VM management session lock ownership is invalid'
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
acquired = lock.flock(File::LOCK_EX | File::LOCK_NB)
|
|
297
|
+
acquired ? lock : nil
|
|
298
|
+
ensure
|
|
299
|
+
lock&.close unless acquired
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def unlink_management_lock(path)
|
|
303
|
+
metadata = path.lstat
|
|
304
|
+
unless metadata.file? && metadata.uid == Process.uid && metadata.mode.nobits?(0o077)
|
|
305
|
+
raise Providers::OwnershipError, 'VM management session lock ownership is invalid'
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
File.unlink(path)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def console_device(hostname)
|
|
312
|
+
"socket,id=console,path=#{escape(console_path(hostname))},server=on,wait=off," \
|
|
313
|
+
"logfile=#{escape(serial_log(hostname))},logappend=on"
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def boot_drives(overlay, seed)
|
|
317
|
+
['-drive', "file=#{escape(overlay)},if=virtio,format=qcow2,cache=none",
|
|
318
|
+
'-drive', "file=#{escape(seed)},if=virtio,format=raw,readonly=on"]
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def firmware(architecture)
|
|
322
|
+
return [] unless architecture == :arm64
|
|
323
|
+
|
|
324
|
+
['-bios', @engine.firmware.to_s]
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def escape(path)
|
|
328
|
+
path.to_s.gsub(',', ',,')
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def monitor_command(record, command)
|
|
332
|
+
Timeout.timeout(3) do
|
|
333
|
+
UNIXSocket.open(monitor_path(record.fetch('hostname'))) do |socket|
|
|
334
|
+
read_monitor_prompt(socket)
|
|
335
|
+
verify_monitor_identity!(socket, record)
|
|
336
|
+
socket.write("#{command}\n")
|
|
337
|
+
command == 'quit' ? wait_monitor_close(socket) : read_monitor_prompt(socket)
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
rescue SystemCallError, IOError, Timeout::Error
|
|
341
|
+
raise Error, 'Cannot control the recorded QEMU process', cause: nil
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def process_name(record)
|
|
345
|
+
"empeira-#{@context.workspace.id}-#{record.fetch('hostname')}-#{record.fetch('peer').fetch('token')}"
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def verify_monitor_identity!(socket, record)
|
|
349
|
+
socket.write("info name\n")
|
|
350
|
+
return if read_monitor_prompt(socket).include?(process_name(record))
|
|
351
|
+
|
|
352
|
+
raise Providers::OwnershipError, 'QEMU monitor belongs to a different node instance; control refused'
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def wait_monitor_close(socket)
|
|
356
|
+
loop { socket.readpartial(1024) }
|
|
357
|
+
rescue EOFError
|
|
358
|
+
nil
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def read_monitor_prompt(socket)
|
|
362
|
+
buffer = +''
|
|
363
|
+
buffer << socket.readpartial(1024) until buffer.include?('(qemu)') || buffer.bytesize > 16_384
|
|
364
|
+
raise Error, 'Unexpected QEMU monitor response' unless buffer.include?('(qemu)')
|
|
365
|
+
|
|
366
|
+
buffer
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def monitor_path(hostname)
|
|
370
|
+
path = monitor_directory(hostname).join('monitor.sock')
|
|
371
|
+
raise Error, 'Temporary path is too long for a QEMU monitor socket' if path.to_s.bytesize > 100
|
|
372
|
+
|
|
373
|
+
path
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def monitor_directory(hostname)
|
|
377
|
+
identifier = Digest::SHA256.hexdigest("#{@context.workspace.id}:#{hostname}")[0, 24]
|
|
378
|
+
[@context.locations.temporary, @context.locations.home].each do |root|
|
|
379
|
+
directory = root.join("empeira-#{identifier}")
|
|
380
|
+
return directory if directory.join('monitor.sock').to_s.bytesize <= 100
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
raise Error, 'No short private path is available for a QEMU monitor socket'
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def prepare_monitor_directory(hostname)
|
|
387
|
+
directory = monitor_directory(hostname)
|
|
388
|
+
FileUtils.mkdir_p(directory, mode: 0o700)
|
|
389
|
+
metadata = File.lstat(directory)
|
|
390
|
+
return if metadata.directory? && metadata.uid == Process.uid && metadata.mode.nobits?(0o077)
|
|
391
|
+
|
|
392
|
+
raise Error, 'VM monitor directory is not private to the current user'
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def node_directory(hostname)
|
|
396
|
+
@context.locations.workspace(@context.workspace).join('vms', hostname)
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def pid_path(hostname)
|
|
400
|
+
node_directory(hostname).join('qemu.pid')
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module VM
|
|
5
|
+
# Removal validates host ownership and never contacts the guest or verifies base-image contents.
|
|
6
|
+
class Removal
|
|
7
|
+
def initialize(context:, runner:, runtime:, store:)
|
|
8
|
+
@context = context
|
|
9
|
+
@runner = runner
|
|
10
|
+
@runtime = runtime
|
|
11
|
+
@store = store
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def remove(record, state)
|
|
15
|
+
root = @store.directory
|
|
16
|
+
path = root.join('vms', record.fetch('hostname'))
|
|
17
|
+
Infrastructure::OwnedFiles.verify_removable!(path, root: root)
|
|
18
|
+
SessionLock.new(context: @context, record: record).exclusive do
|
|
19
|
+
engine = VM.registry.build('qemu', context: @context, runner: @runner)
|
|
20
|
+
qemu = QemuRuntime.new(engine: engine, runner: @runner, context: @context)
|
|
21
|
+
peer = Network::Peer::Backend.build(context: @context, runtime: @runtime, runner: @runner, store: @store)
|
|
22
|
+
qemu.stop(record)
|
|
23
|
+
peer.destroy(record, state)
|
|
24
|
+
qemu.cleanup(record)
|
|
25
|
+
Infrastructure::OwnedFiles.remove(path, root: root)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module Empeira
|
|
6
|
+
module VM
|
|
7
|
+
# Observe cloud-init growth before package bootstrap without changing guest partitions.
|
|
8
|
+
class RootDisk
|
|
9
|
+
ALIGNMENT_MARGIN = 16 * (1024**2)
|
|
10
|
+
|
|
11
|
+
def initialize(guest:)
|
|
12
|
+
@guest = guest
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def verify!(record, size_gib:)
|
|
16
|
+
data = JSON.parse(observe(record, %w[lsblk --json --bytes --paths --output NAME,TYPE,SIZE,MOUNTPOINT]))
|
|
17
|
+
raise TypeError unless data.is_a?(Hash)
|
|
18
|
+
|
|
19
|
+
root, disk = root_device(data.fetch('blockdevices'))
|
|
20
|
+
verify_partition!(root, disk, size_gib * Disk::GIB)
|
|
21
|
+
verify_filesystem!(record, root)
|
|
22
|
+
rescue JSON::ParserError, KeyError, TypeError, ArgumentError
|
|
23
|
+
raise Error, 'Cannot verify VM root disk growth: malformed guest disk metadata; inspect cloud-init logs',
|
|
24
|
+
cause: nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def verify_filesystem!(record, root)
|
|
30
|
+
capacity = Integer(observe(record, ['df', '--block-size=1', '--output=size', '/']).lines.last, 10)
|
|
31
|
+
root_size = byte_size(root)
|
|
32
|
+
return if capacity.between?(root_size * 95 / 100, root_size)
|
|
33
|
+
|
|
34
|
+
raise Error, "VM root filesystem has not grown: #{root.fetch('name')} partition=#{root_size} bytes, " \
|
|
35
|
+
"filesystem=#{capacity} bytes; inspect cloud-init logs"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def observe(record, arguments)
|
|
39
|
+
result = @guest.run(record, arguments)
|
|
40
|
+
return result.stdout if result.success?
|
|
41
|
+
|
|
42
|
+
details = Execution::Diagnostics.command(result, operation: 'Verify VM root disk growth', tool: arguments.first)
|
|
43
|
+
raise Error, "Cannot verify VM root disk growth\n#{details}", cause: nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def root_device(devices)
|
|
47
|
+
roots = device_pairs(devices).select { |device, _parent| device['mountpoint'] == '/' }
|
|
48
|
+
unless roots.size == 1
|
|
49
|
+
raise Error,
|
|
50
|
+
'Cannot identify a unique VM root partition from lsblk; inspect cloud-init logs'
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
root, disk = roots.first
|
|
54
|
+
unless root['type'] == 'part' && disk && disk['type'] == 'disk'
|
|
55
|
+
raise Error, 'Cannot verify VM root growth for this block-device layout; inspect cloud-init logs'
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
[root, disk]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def device_pairs(devices, parent = nil)
|
|
62
|
+
raise TypeError unless devices.is_a?(Array) && devices.all?(Hash)
|
|
63
|
+
|
|
64
|
+
devices.flat_map { |device| [[device, parent], *device_pairs(device.fetch('children', []), device)] }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def verify_partition!(root, disk, expected)
|
|
68
|
+
free = unpartitioned(disk)
|
|
69
|
+
disk_size = byte_size(disk)
|
|
70
|
+
return if disk_size == expected && free.between?(0, ALIGNMENT_MARGIN)
|
|
71
|
+
|
|
72
|
+
raise Error, "VM root partition has not grown: #{root.fetch('name')}, disk=#{disk_size} bytes, " \
|
|
73
|
+
"expected=#{expected} bytes, unpartitioned=#{free} bytes; inspect cloud-init logs"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def unpartitioned(disk)
|
|
77
|
+
partitions = disk.fetch('children').select { |device| device['type'] == 'part' }
|
|
78
|
+
sizes = [byte_size(disk), *partitions.map { |partition| byte_size(partition) }]
|
|
79
|
+
|
|
80
|
+
sizes.first - sizes.drop(1).sum
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def byte_size(device)
|
|
84
|
+
size = device.fetch('size')
|
|
85
|
+
size = Integer(size, 10) if size.is_a?(String) && size.match?(/\A[0-9]+\z/)
|
|
86
|
+
raise TypeError unless size.is_a?(Integer) && size.positive?
|
|
87
|
+
|
|
88
|
+
size
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module VM
|
|
5
|
+
# Pin one node instance during access without retaining the workspace mutation lock.
|
|
6
|
+
class SessionLock
|
|
7
|
+
def initialize(context:, record:)
|
|
8
|
+
directory = context.locations.workspace(context.workspace)
|
|
9
|
+
@path = directory.join("vm-session-#{record.fetch('peer').fetch('token')}.lock")
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def acquire(shared:)
|
|
13
|
+
lock = open_lock
|
|
14
|
+
mode = shared ? File::LOCK_SH : File::LOCK_EX
|
|
15
|
+
unless lock.flock(mode | File::LOCK_NB)
|
|
16
|
+
raise Infrastructure::Locked, 'VM instance has active access or lifecycle operations; detach and retry'
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
lock.close_on_exec = true
|
|
20
|
+
acquired = true
|
|
21
|
+
lock
|
|
22
|
+
ensure
|
|
23
|
+
lock&.close unless acquired
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def exclusive
|
|
27
|
+
lock = acquire(shared: false)
|
|
28
|
+
yield
|
|
29
|
+
ensure
|
|
30
|
+
lock&.close
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def open_lock
|
|
36
|
+
raise Providers::OwnershipError, 'VM session lock path is unsafe' if @path.dirname.symlink? || @path.symlink?
|
|
37
|
+
|
|
38
|
+
# A successful open passes descriptor ownership to acquire.
|
|
39
|
+
# rubocop:disable-next Style/FileOpen
|
|
40
|
+
lock = File.open(@path, File::RDWR | File::CREAT, 0o600)
|
|
41
|
+
verify_lock!(lock.stat)
|
|
42
|
+
|
|
43
|
+
opened = true
|
|
44
|
+
lock
|
|
45
|
+
ensure
|
|
46
|
+
lock&.close unless opened
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def verify_lock!(metadata)
|
|
50
|
+
return if metadata.file? && metadata.uid == Process.uid && metadata.mode.nobits?(0o077)
|
|
51
|
+
|
|
52
|
+
raise Providers::OwnershipError, 'VM session lock ownership is invalid'
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module VM
|
|
5
|
+
# Direct peer transport; system login never invokes the privileged management channel.
|
|
6
|
+
class SystemSSH
|
|
7
|
+
def initialize(context:, runner:, runtime:, qemu:)
|
|
8
|
+
@context = context
|
|
9
|
+
@runner = runner
|
|
10
|
+
@qemu = qemu
|
|
11
|
+
@peer = Network::Peer::Backend.build(context: context, runner: runner, runtime: runtime,
|
|
12
|
+
store: Infrastructure::Store.new(context: context))
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def session(record, user: nil, identity: nil, port: nil)
|
|
16
|
+
selected = Configuration::SSHPreferences.port!(port || 22)
|
|
17
|
+
raise Error, 'Recorded VM is not running; system SSH refused' unless @qemu.running?(record)
|
|
18
|
+
|
|
19
|
+
proxy = @peer.system_ssh_command(record, port: selected)
|
|
20
|
+
endpoint = record.merge('ssh_port' => selected)
|
|
21
|
+
credentials = Node::SSHCredentials.new(context: @context, runner: @runner, provider: 'vm',
|
|
22
|
+
hostname: record.fetch('hostname'), purpose: :system)
|
|
23
|
+
Node::UserSSH.new(runner: @runner, credentials: credentials, proxy_command: proxy,
|
|
24
|
+
default_user: CloudInit::USER, managed_identity: true,
|
|
25
|
+
home: @context.locations.home).session(endpoint, user: user, identity: identity)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'pathname'
|
|
6
|
+
|
|
7
|
+
module Empeira
|
|
8
|
+
class Workspace
|
|
9
|
+
attr_reader :id, :path
|
|
10
|
+
|
|
11
|
+
def self.recover(path, user_home: Dir.home)
|
|
12
|
+
root = recovery_root(path)
|
|
13
|
+
user = Pathname(user_home).realpath.to_s
|
|
14
|
+
identity = allocate
|
|
15
|
+
identity.instance_variable_set(:@id, Digest::SHA256.hexdigest(JSON.generate([user, root.to_s]))[0, 24].freeze)
|
|
16
|
+
identity.instance_variable_set(:@path, root.freeze)
|
|
17
|
+
identity.freeze
|
|
18
|
+
rescue SystemCallError
|
|
19
|
+
raise ConfigurationError, 'Recovery project path and user home must be accessible'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.recovery_root(path)
|
|
23
|
+
unless path.is_a?(String) && !path.include?("\0") && Pathname(path).absolute?
|
|
24
|
+
raise ConfigurationError, 'Recovery project path must be an absolute former Git root path'
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
root = Pathname(path).cleanpath
|
|
28
|
+
root.exist? ? root.realpath : root
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def initialize(path: Dir.pwd, user_home: Dir.home)
|
|
32
|
+
@path = ProjectRoot.resolve(path).path
|
|
33
|
+
raise ConfigurationError, 'project path must be a directory' unless @path.directory?
|
|
34
|
+
|
|
35
|
+
user = Pathname(user_home).realpath.to_s
|
|
36
|
+
@id = Digest::SHA256.hexdigest(JSON.generate([user, @path.to_s]))[0, 24].freeze
|
|
37
|
+
freeze
|
|
38
|
+
rescue SystemCallError
|
|
39
|
+
raise ConfigurationError, 'project path and user home must exist and be accessible'
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|