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,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'shellwords'
|
|
4
|
+
|
|
5
|
+
module Empeira
|
|
6
|
+
module Node
|
|
7
|
+
class SSHClient
|
|
8
|
+
def initialize(credentials:, proxy_command: nil)
|
|
9
|
+
@credentials = credentials
|
|
10
|
+
@proxy_command = proxy_command
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def proxy_options
|
|
14
|
+
return [] unless @proxy_command
|
|
15
|
+
|
|
16
|
+
['-o', "ProxyCommand=#{Shellwords.join(@proxy_command).gsub('%', '%%')}"]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def known_hosts_option
|
|
20
|
+
path = @credentials.known_hosts.to_s.gsub(/["\\]/) { |character| "\\#{character}" }.gsub('%', '%%')
|
|
21
|
+
"UserKnownHostsFile=\"#{path}\""
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
|
|
5
|
+
module Empeira
|
|
6
|
+
module Node
|
|
7
|
+
# Per-node credentials and known hosts stay in private workspace storage.
|
|
8
|
+
class SSHCredentials
|
|
9
|
+
def initialize(context:, runner:, provider:, hostname:, purpose: nil)
|
|
10
|
+
@runner = runner
|
|
11
|
+
raise ArgumentError, 'Unknown SSH credential scope' unless [nil, :system].include?(purpose)
|
|
12
|
+
|
|
13
|
+
@purpose = purpose
|
|
14
|
+
parent = provider == 'vm' ? 'vms' : 'containers'
|
|
15
|
+
@directory = context.locations.workspace(context.workspace).join(parent, hostname)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
attr_reader :directory
|
|
19
|
+
|
|
20
|
+
def key_path
|
|
21
|
+
directory.join('id_ed25519')
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def public_path
|
|
25
|
+
Pathname("#{key_path}.pub")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def known_hosts
|
|
29
|
+
directory.join(@purpose ? "#{@purpose}_known_hosts" : 'known_hosts')
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def prepare
|
|
33
|
+
prepare_hosts
|
|
34
|
+
generate unless [key_path, public_path].any? { |path| path.exist? || path.symlink? }
|
|
35
|
+
verify!
|
|
36
|
+
public_path
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def prepare_hosts
|
|
40
|
+
raise Error, 'Managed SSH directory must not be a symlink' if directory.symlink?
|
|
41
|
+
|
|
42
|
+
FileUtils.mkdir_p(directory, mode: 0o700)
|
|
43
|
+
File.chmod(0o700, directory)
|
|
44
|
+
raise Error, 'Managed SSH known_hosts is unsafe' if known_hosts.symlink? ||
|
|
45
|
+
(known_hosts.exist? && !known_hosts.file?)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def verify!
|
|
49
|
+
raise Error, 'Managed SSH directory must not be a symlink' if directory.symlink?
|
|
50
|
+
|
|
51
|
+
raise Error, 'Managed SSH key material is missing or unsafe; destroy and recreate the node' unless safe_keys?
|
|
52
|
+
raise Error, 'Managed SSH known_hosts must not be a symlink' if known_hosts.symlink?
|
|
53
|
+
|
|
54
|
+
File.chmod(0o600, key_path)
|
|
55
|
+
File.chmod(0o600, known_hosts) if known_hosts.exist?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def cleanup
|
|
59
|
+
raise Error, 'Managed SSH directory must not be a symlink' if directory.symlink?
|
|
60
|
+
|
|
61
|
+
FileUtils.remove_entry_secure(directory) if directory.exist?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def safe_keys?
|
|
67
|
+
[key_path, public_path].all? { |path| path.file? && !path.symlink? && path.readable? }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def generate
|
|
71
|
+
result = @runner.run('ssh-keygen', arguments: ['-q', '-t', 'ed25519', '-C', 'empeira-managed',
|
|
72
|
+
'-N', '', '-f', key_path.to_s],
|
|
73
|
+
timeout: 15)
|
|
74
|
+
raise Error, 'Cannot create managed SSH key; install openssh-client on the CLI host' unless result.success?
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Node
|
|
5
|
+
# The sole allowed container-node publication is managed SSH on host loopback.
|
|
6
|
+
class SSHEndpoint
|
|
7
|
+
def self.valid?(resource, record)
|
|
8
|
+
return resource.fetch('ports').empty? unless record['ssh_host']
|
|
9
|
+
return false unless record['ssh_host'] == '127.0.0.1'
|
|
10
|
+
return tunnel?(resource, record) if record['ssh_transport'] == 'tunnel'
|
|
11
|
+
|
|
12
|
+
loopback?(resource, record)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.tunnel?(resource, record)
|
|
16
|
+
resource.fetch('ports').empty? && resource.fetch('published_ports', {}).values.all?(&:nil?) &&
|
|
17
|
+
record.dig('definition', 'ports') == [] && record['ssh_port'] == 22
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# rubocop:disable-next Metrics/AbcSize -- Compare configured and observed ports against the recorded endpoint.
|
|
21
|
+
def self.loopback?(resource, record)
|
|
22
|
+
return false unless managed?(record) && resource.fetch('ports').keys == ['22/tcp']
|
|
23
|
+
|
|
24
|
+
bindings = resource.fetch('ports')['22/tcp']
|
|
25
|
+
return false unless binding?(bindings, record['ssh_port'], pending: record['ssh_port'].nil?)
|
|
26
|
+
return true unless resource['state'] == 'running'
|
|
27
|
+
|
|
28
|
+
publications = resource.fetch('published_ports', {})
|
|
29
|
+
publications.keys == ['22/tcp'] && binding?(publications['22/tcp'], record['ssh_port'])
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.managed?(record)
|
|
33
|
+
record['ssh_host'] == '127.0.0.1' && record.dig('definition', 'ports') == ['127.0.0.1::22/tcp']
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.binding?(bindings, port, pending: false)
|
|
37
|
+
return false unless loopback_binding?(bindings)
|
|
38
|
+
|
|
39
|
+
value = bindings[0]['HostPort']
|
|
40
|
+
return true if pending && value == ''
|
|
41
|
+
|
|
42
|
+
valid_port?(value) && (port.nil? || value.to_i == port)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.loopback_binding?(bindings)
|
|
46
|
+
bindings.is_a?(Array) && bindings.size == 1 && bindings[0]['HostIp'] == '127.0.0.1'
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.valid_port?(value)
|
|
50
|
+
value.is_a?(String) && value.match?(/\A[0-9]+\z/) && value.to_i.between?(1, 65_535)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.port(resource)
|
|
54
|
+
Integer(resource.fetch('published_ports').fetch('22/tcp').first.fetch('HostPort'), 10)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pathname'
|
|
4
|
+
|
|
5
|
+
module Empeira
|
|
6
|
+
module Node
|
|
7
|
+
# Validate external private-key paths without reading or copying their contents.
|
|
8
|
+
class SSHIdentity
|
|
9
|
+
def initialize(home:)
|
|
10
|
+
@home = Pathname(home)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def resolve(value)
|
|
14
|
+
path = path_for(value)
|
|
15
|
+
stat = path.lstat
|
|
16
|
+
raise ConfigurationError, 'SSH identity must be a readable regular file, without a symlink' unless
|
|
17
|
+
stat.file? && path.readable?
|
|
18
|
+
raise ConfigurationError, 'SSH identity permissions must exclude group and other access (use chmod 600)' unless
|
|
19
|
+
stat.mode.nobits?(0o077)
|
|
20
|
+
|
|
21
|
+
path.to_s
|
|
22
|
+
rescue SystemCallError
|
|
23
|
+
raise ConfigurationError, 'SSH identity is missing or unreadable', cause: nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def path_for(value)
|
|
29
|
+
unless value.is_a?(String) && !value.empty? && !value.match?(/[\p{Cc}\p{Cf}]/)
|
|
30
|
+
raise ConfigurationError, 'SSH identity must be a nonempty file path without control characters'
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
value.start_with?('~/') ? @home.join(value.delete_prefix('~/')).expand_path : Pathname(value).expand_path
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'etc'
|
|
4
|
+
|
|
5
|
+
module Empeira
|
|
6
|
+
module Node
|
|
7
|
+
# Provider defaults select authentication; endpoint and host-key policy remain managed.
|
|
8
|
+
class UserSSH
|
|
9
|
+
def initialize(runner:, credentials:, proxy_command: nil, default_user: nil, managed_identity: false,
|
|
10
|
+
home: Dir.home)
|
|
11
|
+
@runner = runner
|
|
12
|
+
@credentials = credentials
|
|
13
|
+
@proxy_command = proxy_command
|
|
14
|
+
@default_user = default_user
|
|
15
|
+
@managed_identity = managed_identity
|
|
16
|
+
@identity = SSHIdentity.new(home: home)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def session(record, user: nil, identity: nil)
|
|
20
|
+
@credentials.prepare_hosts
|
|
21
|
+
username = username_for(user)
|
|
22
|
+
transport = SSHClient.new(credentials: @credentials, proxy_command: @proxy_command)
|
|
23
|
+
arguments = ['-tt', '-l', username, *options(record, transport), *authentication_options(identity)]
|
|
24
|
+
@runner.stream('ssh', arguments: [*arguments, record.fetch('hostname')])
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def username_for(user)
|
|
28
|
+
username = user || @default_user || Etc.getpwuid(Process.uid).name
|
|
29
|
+
unless username.is_a?(String) && username.match?(/\A[a-zA-Z0-9_.@-]+\z/)
|
|
30
|
+
raise ConfigurationError, 'SSH user must be a valid login name'
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
username
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def authentication_options(identity)
|
|
37
|
+
return identity ? ['-i', @identity.resolve(identity)] : [] unless @managed_identity
|
|
38
|
+
|
|
39
|
+
@credentials.verify! unless identity
|
|
40
|
+
key = identity ? @identity.resolve(identity) : @credentials.key_path.to_s
|
|
41
|
+
['-i', key, '-o', 'IdentitiesOnly=yes', '-o', 'PasswordAuthentication=no',
|
|
42
|
+
'-o', 'KbdInteractiveAuthentication=no', '-o', 'ForwardAgent=no']
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def options(record, transport)
|
|
46
|
+
port = record['ssh_port']
|
|
47
|
+
unless record.fetch('ssh_host', '127.0.0.1') == '127.0.0.1' && port.is_a?(Integer) && port.between?(1, 65_535)
|
|
48
|
+
raise Error, 'Managed SSH endpoint is missing or inconsistent'
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
proxy = @proxy_command ? transport.proxy_options : ['-o', 'ProxyCommand=none']
|
|
52
|
+
['-p', port.to_s, '-o', 'HostName=127.0.0.1', '-o', "HostKeyAlias=#{record.fetch('hostname')}",
|
|
53
|
+
'-o', transport.known_hosts_option, '-o', "GlobalKnownHostsFile=#{File::NULL}",
|
|
54
|
+
'-o', 'StrictHostKeyChecking=accept-new', '-o', 'CanonicalizeHostname=no',
|
|
55
|
+
'-o', 'ControlMaster=no', '-o', 'ControlPath=none', '-o', 'ClearAllForwardings=yes',
|
|
56
|
+
*proxy, '-o', 'ProxyJump=none']
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Node
|
|
5
|
+
# Public logical node provider; QEMU and container networking stay replaceable.
|
|
6
|
+
class VM < Interface
|
|
7
|
+
include VMState
|
|
8
|
+
include VMPreflight
|
|
9
|
+
include VMPreparation
|
|
10
|
+
include VMLifecycle
|
|
11
|
+
|
|
12
|
+
def initialize(context:, runner:, backend:, runtime:, build_info: BuildInfo.load, progress: Progress.new)
|
|
13
|
+
super(name: 'vm', context: context, runner: runner, backend: backend)
|
|
14
|
+
@engine = backend
|
|
15
|
+
@runtime = runtime
|
|
16
|
+
@build_info = build_info
|
|
17
|
+
@progress = progress
|
|
18
|
+
@store = Infrastructure::Store.new(context: context)
|
|
19
|
+
prepare_components(context, runner, backend, runtime)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# rubocop:disable-next Naming/PredicateMethod -- Reconciliation mutates guest resources.
|
|
23
|
+
def reconcile_all(state:)
|
|
24
|
+
load_state(state)
|
|
25
|
+
vm_records.values.map do |record|
|
|
26
|
+
record['provisioned'] && @qemu.running?(record) ? reconcile_guest(record) : false
|
|
27
|
+
end.any?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def validate_interfaces(record)
|
|
33
|
+
::Empeira::VM::Interfaces.validate_static!(context, record.fetch('hostname'), @state)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def reconcile_interfaces(record)
|
|
37
|
+
::Empeira::VM::Interfaces.new(context: context, record: record, state: @state, guest: @guest,
|
|
38
|
+
persist: method(:save)).reconcile
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def reconcile_guest(record)
|
|
42
|
+
fact_changed = reconcile_provider_fact(record)
|
|
43
|
+
proxy_changed = reconcile_runtime_proxy(record)
|
|
44
|
+
tools_changed = reconcile_interactive_tools(record)
|
|
45
|
+
interfaces_changed = reconcile_interfaces(record)
|
|
46
|
+
reconcile_command_mocks(record) || interfaces_changed || proxy_changed || tools_changed || fact_changed
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# rubocop:disable-next Naming/PredicateMethod -- Reconciliation mutates owned guest state and reports changes.
|
|
50
|
+
def reconcile_provider_fact(record)
|
|
51
|
+
digest = ExternalFact.fingerprint('vm')
|
|
52
|
+
return false if record['empeira_fact'] == digest
|
|
53
|
+
|
|
54
|
+
VMBootstrap.new(guest: @guest).reconcile(record: record, bootstrap: Bootstrap.new(provider: 'vm'))
|
|
55
|
+
record['empeira_fact'] = digest
|
|
56
|
+
save
|
|
57
|
+
true
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def reconcile_interactive_tools(record)
|
|
61
|
+
InteractiveTools.new(record: record, persist: method(:save),
|
|
62
|
+
execute: ->(arguments) { @guest.run(record, arguments) }).reconcile
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def reconcile_runtime_proxy(record)
|
|
66
|
+
RuntimeProxy.new(context: context, record: record, persist: method(:save),
|
|
67
|
+
execute: ->(arguments) { @guest.run(record, arguments) }).reconcile
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def reconcile_command_mocks(record)
|
|
71
|
+
CommandMocks.new(context: context, record: record, persist: method(:save),
|
|
72
|
+
execute: ->(arguments) { @guest.run(record, arguments) }).reconcile
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def prepare_components(context, runner, backend, runtime)
|
|
76
|
+
@peer = Network::Peer::Backend.build(context: context, runtime: runtime, runner: runner, store: @store)
|
|
77
|
+
@bootstrap_proxy = Network::BootstrapProxy.new(context: context, runtime: runtime, store: @store)
|
|
78
|
+
@source = ::Empeira::VM::ImageSource.new
|
|
79
|
+
@cache = ::Empeira::VM::ImageCache.new(locations: context.locations)
|
|
80
|
+
@disk = ::Empeira::VM::Disk.new(engine: backend, runner: runner,
|
|
81
|
+
workspace_directory: context.locations.workspace(context.workspace))
|
|
82
|
+
@qemu = ::Empeira::VM::QemuRuntime.new(engine: backend, runner: runner, context: context)
|
|
83
|
+
@cloud = ::Empeira::VM::CloudInit.new(context: context, runner: runner)
|
|
84
|
+
@guest = ::Empeira::VM::Guest.new(context: context, runner: runner, qemu: @qemu)
|
|
85
|
+
@agent = ::Empeira::VM::Agent.new(context: context, guest: @guest, runtime: runtime, progress: @progress)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Node
|
|
5
|
+
# Cloud-Init installs shared files; management transport verifies them before Puppet.
|
|
6
|
+
class VMBootstrap
|
|
7
|
+
def initialize(guest:)
|
|
8
|
+
@guest = guest
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def verify(record:, bootstrap:)
|
|
12
|
+
bootstrap.files.each { |file| verify_file!(record, file) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# rubocop:disable-next Naming/PredicateMethod, Metrics/AbcSize -- Verify, atomically upload and verify the owned fact.
|
|
16
|
+
def reconcile(record:, bootstrap:)
|
|
17
|
+
bootstrap.files.map do |file|
|
|
18
|
+
content = @guest.run(record, ['cat', file.path])
|
|
19
|
+
mode = @guest.run(record, ['stat', '-c', '%a', file.path])
|
|
20
|
+
next false if valid_file?(content, mode, file)
|
|
21
|
+
|
|
22
|
+
Tempfile.create('empeira-fact') do |source|
|
|
23
|
+
source.write(file.content)
|
|
24
|
+
source.flush
|
|
25
|
+
@guest.copy_to(record, source.path, file.path, mode: format('%04o', file.mode))
|
|
26
|
+
end
|
|
27
|
+
verify_file!(record, file)
|
|
28
|
+
true
|
|
29
|
+
end.any?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def verify_file!(record, file)
|
|
35
|
+
content = @guest.run(record, ['cat', file.path])
|
|
36
|
+
mode = @guest.run(record, ['stat', '-c', '%a', file.path])
|
|
37
|
+
return if valid_file?(content, mode, file)
|
|
38
|
+
|
|
39
|
+
details = [file_diagnostic(content, file, 'content', 'cat'), file_diagnostic(mode, file, 'mode', 'stat')]
|
|
40
|
+
raise Error, "Cannot verify required Empeira VM bootstrap file #{file.path}; Puppet was not run\n" \
|
|
41
|
+
"#{details.join("\n")}", cause: nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def file_diagnostic(result, file, field, tool)
|
|
45
|
+
Execution::Diagnostics.command(result, operation: "Bootstrap file #{file.path}: #{field}", tool: tool)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def valid_file?(content, mode, file)
|
|
49
|
+
content.success? && content.stdout == file.content && mode.success? && mode.stdout.strip.to_i(8) == file.mode
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
|
|
5
|
+
module Empeira
|
|
6
|
+
module Node
|
|
7
|
+
# rubocop:disable-next Metrics/ModuleLength -- Node lifecycle and cleanup share the workspace lock.
|
|
8
|
+
module VMLifecycle
|
|
9
|
+
def start(name:)
|
|
10
|
+
mutate do
|
|
11
|
+
record = fetch_vm(name)
|
|
12
|
+
unless record['provisioned']
|
|
13
|
+
raise Error, 'VM bootstrap is incomplete; inspect logs, then destroy and recreate the node'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
validate_interfaces(record)
|
|
17
|
+
ready_server
|
|
18
|
+
return lifecycle_result(name.downcase, :running, changed: reconcile_guest(record)) if @qemu.running?(record)
|
|
19
|
+
|
|
20
|
+
@progress.stage(1, 'Checking VM control-plane readiness...')
|
|
21
|
+
@engine.preflight!(progress: @progress, seeds: false)
|
|
22
|
+
boot_existing(record)
|
|
23
|
+
reconcile_guest(record)
|
|
24
|
+
lifecycle_result(record.fetch('hostname'), :running, changed: true)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def stop(name:)
|
|
29
|
+
mutate do
|
|
30
|
+
record = fetch_vm(name)
|
|
31
|
+
session_lock(record).exclusive do
|
|
32
|
+
changed = !!@qemu.stop(record)
|
|
33
|
+
@peer.stop(record)
|
|
34
|
+
record['state'] = 'stopped'
|
|
35
|
+
record['pid'] = nil
|
|
36
|
+
save
|
|
37
|
+
lifecycle_result(record.fetch('hostname'), :stopped, changed: changed)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def destroy(name:)
|
|
43
|
+
Infrastructure::Cleanup.new(context: context, runner: @runner, runtime: @runtime,
|
|
44
|
+
progress: @progress).node(name)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def destroy_all(state:)
|
|
48
|
+
load_state(state)
|
|
49
|
+
@runtime.check_available!
|
|
50
|
+
@bootstrap_proxy.cleanup(@state)
|
|
51
|
+
vm_records.each_key { |name| remove_vm(name, certificates: false) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def puppet(name:)
|
|
55
|
+
mutate(availability: false) do
|
|
56
|
+
record = running_vm(name)
|
|
57
|
+
@progress.stage(30, 'Running Puppet in VM...')
|
|
58
|
+
puppet_run(record)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def shell(name:)
|
|
63
|
+
interactive_vm(name) { |record| @guest.shell(record) }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def console(name:)
|
|
67
|
+
interactive_vm(name) { |record| @qemu.console(record) }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def exec(name:, arguments:, timeout: nil, output: $stdout, error: $stderr)
|
|
71
|
+
DiagnosticCommand.validate!(arguments, timeout)
|
|
72
|
+
interactive_vm(name) do |record|
|
|
73
|
+
@guest.stream(record, arguments, timeout: timeout, output: output, error: error)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def ssh(name:, user: nil, identity: nil, port: nil)
|
|
78
|
+
interactive_vm(name) do |record|
|
|
79
|
+
::Empeira::VM::SystemSSH.new(context: context, runner: @runner, runtime: @runtime, qemu: @qemu)
|
|
80
|
+
.session(record, user: user, identity: identity, port: port)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def logs(name:)
|
|
85
|
+
load_state
|
|
86
|
+
record = fetch_vm(name)
|
|
87
|
+
path = @qemu.serial_log(record.fetch('hostname'))
|
|
88
|
+
raise Error, 'VM serial log is unavailable' unless path.file?
|
|
89
|
+
|
|
90
|
+
@runner.stream('tail', arguments: ['-n', '200', '-f', path.to_s])
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def interactive_vm(name)
|
|
96
|
+
lock = nil
|
|
97
|
+
record = mutate do
|
|
98
|
+
current = booted_vm(name)
|
|
99
|
+
lock = session_lock(current).acquire(shared: true)
|
|
100
|
+
current
|
|
101
|
+
end
|
|
102
|
+
Execution::Terminal.with_interrupts { @progress.streaming { yield record } }
|
|
103
|
+
ensure
|
|
104
|
+
lock&.close
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def session_lock(record)
|
|
108
|
+
::Empeira::VM::SessionLock.new(context: context, record: record)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def boot_existing(record)
|
|
112
|
+
session_lock(record).exclusive { resume_vm(record) }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# rubocop:disable-next Metrics/AbcSize, Metrics/MethodLength -- Retained VM restart has ordered, visible milestones.
|
|
116
|
+
def resume_vm(record)
|
|
117
|
+
@progress.stage(20, 'Checking retained VM storage...')
|
|
118
|
+
overlay, seed = existing_storage(record)
|
|
119
|
+
@progress.stage(30, 'Checking VM network prerequisites...')
|
|
120
|
+
@peer.preflight(@state)
|
|
121
|
+
@progress.stage(40, 'Preparing VM network...')
|
|
122
|
+
@peer.prepare(record, @state)
|
|
123
|
+
attachment = Network::Peer::Attachment.new(backend: @peer, record: record, state: @state)
|
|
124
|
+
begin
|
|
125
|
+
@progress.stage(55, 'Starting virtual machine...')
|
|
126
|
+
record['pid'] = @qemu.launch(record: record, overlay: overlay, seed: seed, network: attachment)
|
|
127
|
+
ensure
|
|
128
|
+
save if record['pid']
|
|
129
|
+
end
|
|
130
|
+
@progress.stage(80, 'Waiting for VM management readiness...')
|
|
131
|
+
@guest.wait(record, progress: @progress)
|
|
132
|
+
record['state'] = 'running'
|
|
133
|
+
save
|
|
134
|
+
@progress.stage(95, 'Virtual machine ready.')
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def existing_storage(record)
|
|
138
|
+
::Empeira::VM::Management.validate!(record)
|
|
139
|
+
base = base_for(record)
|
|
140
|
+
unless base.file? && !base.symlink? &&
|
|
141
|
+
OpenSSL::Digest::SHA256.file(base).hexdigest == record.dig('base_image', 'checksum')
|
|
142
|
+
raise Error, 'Pinned VM base image is missing or corrupt; preserve the overlay and restore the cache'
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
overlay = @disk.verify!(hostname: record.fetch('hostname'), base: base)
|
|
146
|
+
seed = vm_directory(record).join('seed.iso')
|
|
147
|
+
raise Error, 'VM cloud-init seed is missing; preserve the node for recovery' unless seed.file?
|
|
148
|
+
|
|
149
|
+
[overlay, seed]
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def running_vm(name)
|
|
153
|
+
record = booted_vm(name)
|
|
154
|
+
unless record['provisioned']
|
|
155
|
+
raise Error,
|
|
156
|
+
'VM bootstrap is incomplete; inspect logs or shell before recreating it'
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
record
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def booted_vm(name)
|
|
163
|
+
record = fetch_vm(name)
|
|
164
|
+
unless @qemu.running?(record)
|
|
165
|
+
raise Error, "VM #{record.fetch('hostname')} is stopped; run empeira node start #{record.fetch('hostname')}"
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
record
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def remove_vm(name, certificates: true)
|
|
172
|
+
record = vm_records[name]
|
|
173
|
+
return unless record
|
|
174
|
+
|
|
175
|
+
session_lock(record).exclusive { remove_vm_instance(name, record, certificates: certificates) }
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def remove_vm_instance(name, record, certificates:)
|
|
179
|
+
@qemu.stop(record)
|
|
180
|
+
@peer.destroy(record, @state)
|
|
181
|
+
@qemu.cleanup(record)
|
|
182
|
+
clean_vm_certificate(record) if certificates && record['certificate_key']
|
|
183
|
+
remove_vm_disk(record)
|
|
184
|
+
remove_vm_directory(record)
|
|
185
|
+
@nodes.delete(name)
|
|
186
|
+
save
|
|
187
|
+
record
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def clean_vm_certificate(record)
|
|
191
|
+
Certificates.new(runtime: @runtime, server: ready_server).clean(record)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def remove_vm_disk(record)
|
|
195
|
+
base = base_for(record)
|
|
196
|
+
overlay = vm_directory(record).join('disk.qcow2')
|
|
197
|
+
@disk.remove(hostname: record.fetch('hostname'), base: base) if overlay.exist? || overlay.symlink?
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def remove_vm_directory(record)
|
|
201
|
+
directory = vm_directory(record)
|
|
202
|
+
return unless directory.exist? || directory.symlink?
|
|
203
|
+
|
|
204
|
+
FileUtils.remove_entry_secure(directory)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def vm_directory(record)
|
|
208
|
+
context.locations.workspace(context.workspace).join('vms', record.fetch('hostname'))
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Node
|
|
5
|
+
module VMPreflight
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def preflight_vm(request)
|
|
9
|
+
reject_vm_name!(request.hostname.downcase)
|
|
10
|
+
::Empeira::VM::Interfaces.validate_static!(context, request.hostname, @state)
|
|
11
|
+
accelerator = @engine.preflight!(progress: @progress)
|
|
12
|
+
@progress.stage(15, 'Validating guest image and bootstrap requirements...')
|
|
13
|
+
validate_bootstrap(request)
|
|
14
|
+
image = @source.resolve(distribution: request.os, version: request.version,
|
|
15
|
+
architecture: context.platform.architecture)
|
|
16
|
+
[image, accelerator]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def validate_bootstrap(request)
|
|
20
|
+
@cloud.validate!
|
|
21
|
+
packages = preflight_packages(request)
|
|
22
|
+
@requirements = ::Empeira::VM::BootstrapRequirements.new(context: context, os: request.os,
|
|
23
|
+
version: request.version,
|
|
24
|
+
distribution_required: packages.required?)
|
|
25
|
+
::Empeira::Agent::Acquisition.new(context: context, runtime: @runtime).preflight!(@requirements)
|
|
26
|
+
@peer.preflight(@state)
|
|
27
|
+
@bootstrap_proxy.preflight!(@state) if @requirements.destinations.any?
|
|
28
|
+
@progress.heartbeat(@requirements.report)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def preflight_packages(request)
|
|
32
|
+
PackageBootstrap.new(config: context.configuration.dig('bootstrap', 'packages'),
|
|
33
|
+
os: request.os, execute: nil)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|