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,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Node
|
|
5
|
+
class Certificates
|
|
6
|
+
PUPPET = '/opt/puppetlabs/bin/puppet'
|
|
7
|
+
CA = '/opt/puppetlabs/bin/puppetserver'
|
|
8
|
+
RUBY = '/opt/puppetlabs/puppet/bin/ruby'
|
|
9
|
+
|
|
10
|
+
def initialize(runtime:, server:)
|
|
11
|
+
@runtime = runtime
|
|
12
|
+
@server = server
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def enroll(node, record)
|
|
16
|
+
name = record.fetch('hostname')
|
|
17
|
+
raise Error, 'A CA identity already exists for this hostname; refusing to adopt it' if entry(name)
|
|
18
|
+
|
|
19
|
+
install_ca(node)
|
|
20
|
+
execute(node, [PUPPET, 'ssl', 'generate_request'], 'Node key preparation')
|
|
21
|
+
fingerprint = public_key(node, "/etc/puppetlabs/puppet/ssl/certificate_requests/#{name}.pem", request: true)
|
|
22
|
+
record['certificate_key'] = fingerprint
|
|
23
|
+
yield
|
|
24
|
+
execute(node, [PUPPET, 'ssl', 'submit_request'], 'Node certificate request')
|
|
25
|
+
request = entry(name)
|
|
26
|
+
unless request && fingerprint == ca_key(name, request: true)
|
|
27
|
+
raise Error, 'CA request does not match the Empeira-owned node key; signing refused'
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
execute(@server, [CA, 'ca', 'sign', '--certname', name], 'Node certificate signing')
|
|
31
|
+
execute(node, [PUPPET, 'ssl', 'download_cert'], 'Node certificate retrieval')
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def clean(record)
|
|
35
|
+
certificate = entry(record.fetch('hostname'))
|
|
36
|
+
return unless certificate
|
|
37
|
+
|
|
38
|
+
request = certificate.fetch('state') == 'requested'
|
|
39
|
+
unless record['certificate_key'] && record['certificate_key'] == ca_key(record.fetch('hostname'),
|
|
40
|
+
request: request)
|
|
41
|
+
raise Error, 'CA identity differs from the recorded node key; certificate cleanup refused'
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
execute(@server, [CA, 'ca', 'clean', '--certname', record.fetch('hostname')], 'Node certificate cleanup')
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def clean_retired(state, name)
|
|
48
|
+
retired = state.fetch('retired_certificates', {})
|
|
49
|
+
return unless retired.key?(name)
|
|
50
|
+
|
|
51
|
+
clean('hostname' => name, 'certificate_key' => retired.fetch(name))
|
|
52
|
+
retired.delete(name)
|
|
53
|
+
yield
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def entry(name)
|
|
59
|
+
result = execute(@server, [CA, 'ca', 'list', '--all', '--format', 'json'], 'CA inventory')
|
|
60
|
+
data = JSON.parse(result.stdout)
|
|
61
|
+
data.values.flatten.find { |item| item.fetch('name') == name }
|
|
62
|
+
rescue JSON::ParserError, NoMethodError, KeyError
|
|
63
|
+
raise Error, 'Malformed CA inventory; certificate mutation refused', cause: nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def ca_key(name, request:)
|
|
67
|
+
directory = request ? 'requests' : 'signed'
|
|
68
|
+
public_key(@server, "/etc/puppetlabs/puppetserver/ca/#{directory}/#{name}.pem", request: request)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def public_key(resource, path, request:)
|
|
72
|
+
type = request ? 'Request' : 'Certificate'
|
|
73
|
+
code = "require 'openssl'; require 'digest'; cert = OpenSSL::X509::#{type}.new(File.read(ARGV[0])); print Digest::SHA256.hexdigest(cert.public_key.to_der)"
|
|
74
|
+
execute(resource, [RUBY, '-e', code, path], 'Certificate identity verification').stdout
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def install_ca(node)
|
|
78
|
+
Dir.mktmpdir('empeira-ca-') do |directory|
|
|
79
|
+
file = File.join(directory, 'ca.pem')
|
|
80
|
+
@runtime.copy_from(@server, '/etc/puppetlabs/puppet/ssl/certs/ca.pem', file)
|
|
81
|
+
execute(node, ['mkdir', '-p', '/etc/puppetlabs/puppet/ssl/certs'], 'Node CA directory')
|
|
82
|
+
@runtime.copy_to(node, file, '/etc/puppetlabs/puppet/ssl/certs/ca.pem')
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def execute(resource, arguments, operation)
|
|
87
|
+
result = @runtime.service_exec(resource, arguments, timeout: 60)
|
|
88
|
+
unless result.success?
|
|
89
|
+
diagnostic = Execution::Diagnostics.native(result, operation: operation,
|
|
90
|
+
tool: File.basename(arguments.first))
|
|
91
|
+
raise Error, "#{operation} failed; node state retained for diagnosis\n#{diagnostic}", cause: nil
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
result
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'shellwords'
|
|
4
|
+
|
|
5
|
+
module Empeira
|
|
6
|
+
module Node
|
|
7
|
+
# A guest file resource with the same ownership labels and fingerprint contract as services.
|
|
8
|
+
class CommandMock < Services::Definition
|
|
9
|
+
attr_reader :command
|
|
10
|
+
|
|
11
|
+
def initialize(command:, workspace:, record:, definition:)
|
|
12
|
+
@command = command
|
|
13
|
+
super(key: "command-mock-#{command}", workspace: workspace, **definition.transform_keys(&:to_sym),
|
|
14
|
+
hostname: record.fetch('hostname'), provider: record.fetch('provider'))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def ownership_labels
|
|
18
|
+
super.merge('io.empeira.hostname' => options.fetch('hostname'),
|
|
19
|
+
'io.empeira.provider' => options.fetch('provider'))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def file
|
|
23
|
+
Bootstrap::FileEntry.new(path: options.fetch('path'), content: content, mode: 0o755).freeze
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def content
|
|
27
|
+
invocation = if options.fetch('mock_to') == 'echo'
|
|
28
|
+
"printf '%s' #{Shellwords.escape(command)}\n" \
|
|
29
|
+
"[ \"$#\" -eq 0 ] || printf ' %s' \"$@\"\nprintf '\\n'\n"
|
|
30
|
+
else
|
|
31
|
+
"#{Shellwords.escape(options.fetch('mock_to'))} \"$@\"\n"
|
|
32
|
+
end
|
|
33
|
+
status = options.fetch('exit_code') == 'passthrough' ? '$?' : options.fetch('exit_code').to_s
|
|
34
|
+
"#!/bin/sh\n# #{JSON.generate(labels)}\n#{invocation}exit #{status}\n"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Node
|
|
5
|
+
# Reconciles guest resources under the existing workspace transaction.
|
|
6
|
+
class CommandMocks
|
|
7
|
+
def initialize(context:, record:, execute:, persist:)
|
|
8
|
+
@context = context
|
|
9
|
+
@record = record
|
|
10
|
+
@execute = execute
|
|
11
|
+
@persist = persist
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def reconcile
|
|
15
|
+
desired = @context.configuration.fetch('mocks').fetch('commands')
|
|
16
|
+
return false if desired.empty? && @record.fetch('command_mocks', {}).empty?
|
|
17
|
+
|
|
18
|
+
@entries = @record['command_mocks'] ||= {}
|
|
19
|
+
changed = prune(desired)
|
|
20
|
+
desired.each do |name, definition|
|
|
21
|
+
changed = apply(name, definition) || changed
|
|
22
|
+
end
|
|
23
|
+
changed
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.valid_inventory?(entries)
|
|
27
|
+
return false unless entries.is_a?(Hash)
|
|
28
|
+
|
|
29
|
+
entries.all? { |path, entry| valid_entry?(path, entry) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.valid_entry?(path, entry)
|
|
33
|
+
entry.is_a?(Hash) && (entry.keys - %w[name definition fingerprint previous]).empty? &&
|
|
34
|
+
valid_definition?(path, entry['name'], entry['definition']) &&
|
|
35
|
+
entry['fingerprint'] == Infrastructure::Definition.fingerprint(entry['definition']) &&
|
|
36
|
+
(!entry.key?('previous') || valid_definition?(path, entry['name'], entry['previous']))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.valid_definition?(path, name, definition)
|
|
40
|
+
Configuration::CommandMocks.validate!({ name => definition }, effective: true)
|
|
41
|
+
definition['path'] == path
|
|
42
|
+
rescue ConfigurationError
|
|
43
|
+
false
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def resource(name, definition)
|
|
49
|
+
CommandMock.new(command: name, definition: definition, workspace: @context.workspace, record: @record)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def accepted(entry)
|
|
53
|
+
[entry.fetch('definition'), entry['previous']].compact.map do |definition|
|
|
54
|
+
resource(entry.fetch('name'), definition).file.content
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def prune(desired)
|
|
59
|
+
changed = false
|
|
60
|
+
@entries.each_key do |path|
|
|
61
|
+
entry = @entries.fetch(path)
|
|
62
|
+
next if desired.dig(entry.fetch('name'), 'path') == path
|
|
63
|
+
|
|
64
|
+
changed = execute(path, nil, accepted(entry)) || changed
|
|
65
|
+
@entries.delete(path)
|
|
66
|
+
@persist.call
|
|
67
|
+
end
|
|
68
|
+
changed
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def apply(name, definition)
|
|
72
|
+
path = definition.fetch('path')
|
|
73
|
+
entry = @entries[path]
|
|
74
|
+
return finish(entry) if entry && entry['definition'] == definition
|
|
75
|
+
|
|
76
|
+
# Finish interrupted writes before accepting another desired revision.
|
|
77
|
+
recovered = entry&.key?('previous') ? finish(entry) : false
|
|
78
|
+
entry = { 'name' => name, 'definition' => definition,
|
|
79
|
+
'fingerprint' => Infrastructure::Definition.fingerprint(definition),
|
|
80
|
+
**(entry ? { 'previous' => entry.fetch('definition') } : {}) }
|
|
81
|
+
@entries[path] = entry
|
|
82
|
+
@persist.call
|
|
83
|
+
finish(entry) || recovered
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def finish(entry)
|
|
87
|
+
file = resource(entry.fetch('name'), entry.fetch('definition')).file
|
|
88
|
+
changed = execute(file.path, file.content, accepted(entry))
|
|
89
|
+
@persist.call if entry.delete('previous')
|
|
90
|
+
changed
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def execute(path, content, accepted)
|
|
94
|
+
helper = File.read(File.expand_path('../../../resources/nodes/managed_file.rb', __dir__))
|
|
95
|
+
request = JSON.generate('path' => path, 'content' => content, 'accepted' => accepted, 'mode' => 0o755)
|
|
96
|
+
result = @execute.call([Certificates::RUBY, '-e', helper, request])
|
|
97
|
+
unless result.success?
|
|
98
|
+
unless @execute.call([Certificates::RUBY, '--version']).success?
|
|
99
|
+
raise Error,
|
|
100
|
+
"Cannot run node agent Ruby at #{Certificates::RUBY}; " \
|
|
101
|
+
'the agent installation or guest execution is unavailable. Inspect the node and its image.'
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
raise Providers::OwnershipError,
|
|
105
|
+
"Cannot reconcile command mock at #{path}: unsafe target, cleanup conflict or guest I/O failure"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
JSON.parse(result.stdout).fetch('changed')
|
|
109
|
+
rescue JSON::ParserError, KeyError
|
|
110
|
+
raise Error, "Cannot verify command-mock reconciliation at #{path}"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Node
|
|
5
|
+
# Container provider owns its workflow; the public service only routes logical providers.
|
|
6
|
+
# rubocop:disable-next Metrics/ClassLength -- Ordered provisioning and lifecycle share this provider boundary.
|
|
7
|
+
class Container < Interface
|
|
8
|
+
include ContainerLifecycle
|
|
9
|
+
include ContainerPreparation
|
|
10
|
+
include ContainerAgentBootstrap
|
|
11
|
+
include ContainerState
|
|
12
|
+
include ContainerGuest
|
|
13
|
+
|
|
14
|
+
attr_reader :context
|
|
15
|
+
|
|
16
|
+
def initialize(context:, runner:, backend:, build_info: BuildInfo.load, progress: Progress.new)
|
|
17
|
+
super(name: 'container', context: context, runner: runner, backend: backend)
|
|
18
|
+
@runtime = backend
|
|
19
|
+
@build_info = build_info
|
|
20
|
+
@progress = progress
|
|
21
|
+
@store = Infrastructure::Store.new(context: context)
|
|
22
|
+
@bootstrap_proxy = Network::BootstrapProxy.new(context: context, runtime: @runtime, store: @store)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def available?
|
|
26
|
+
@runtime.available?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# rubocop:disable-next Metrics/AbcSize, Metrics/MethodLength -- Verify retired certificate ownership before the existing enrollment transaction.
|
|
30
|
+
def run(request)
|
|
31
|
+
mutate do
|
|
32
|
+
@progress.stage(10, 'Checking control plane and reserving hostname...')
|
|
33
|
+
server = ready_server
|
|
34
|
+
record, definition, image = reserve(request)
|
|
35
|
+
prepare_image(image, record)
|
|
36
|
+
resource = create_node(definition, record)
|
|
37
|
+
@progress.stage(65, 'Preparing node certificate...')
|
|
38
|
+
certificates = Certificates.new(runtime: @runtime, server: server)
|
|
39
|
+
certificates.clean_retired(@state, record.fetch('hostname')) { save } if @state['retired_certificates']
|
|
40
|
+
certificates.enroll(resource, record) { save }
|
|
41
|
+
reconcile_guest(resource, record)
|
|
42
|
+
complete_provisioning(record)
|
|
43
|
+
@progress.stage(85, 'Running Puppet agent...')
|
|
44
|
+
agent_run(resource, record)
|
|
45
|
+
lifecycle_result(record['hostname'], :running, changed: true)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def destroy_all(state:)
|
|
50
|
+
@state = state
|
|
51
|
+
@nodes = state.fetch('nodes', {})
|
|
52
|
+
@nodes.each { |name, record| remove_node(name, certificates: false) if record['provider'] == 'container' }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# rubocop:disable-next Naming/PredicateMethod -- Reconciliation mutates guest resources.
|
|
56
|
+
def reconcile_all(state:)
|
|
57
|
+
@state = state
|
|
58
|
+
@nodes = state.fetch('nodes', {})
|
|
59
|
+
container_records.values.map do |record|
|
|
60
|
+
resource = observed(record)
|
|
61
|
+
network_gateway.route(resource) if resource && resource['state'] == 'running'
|
|
62
|
+
next false unless record['provisioned'] && resource && resource['state'] == 'running'
|
|
63
|
+
|
|
64
|
+
reconcile_guest(resource, record)
|
|
65
|
+
end.any?
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def complete_provisioning(record)
|
|
71
|
+
record['provisioned'] = true
|
|
72
|
+
save
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def reconcile_command_mocks(resource, record)
|
|
76
|
+
CommandMocks.new(context: context, record: record, persist: method(:save),
|
|
77
|
+
execute: ->(arguments) { @runtime.service_exec(resource, arguments) }).reconcile
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def prepare_image(image, record)
|
|
81
|
+
@progress.stage(25, 'Preparing node image...')
|
|
82
|
+
@runtime.with_local_images do
|
|
83
|
+
@runtime.ensure_image(image.reference, recipe: image.recipe, files: image.build_files)
|
|
84
|
+
return if @runtime.image_architecture(image.reference) == record['architecture']
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
raise Error, 'Node image architecture differs from the runtime; emulation is not enabled'
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# rubocop:disable-next Metrics/AbcSize, Metrics/MethodLength -- Ordered bootstrap cleanup precedes normal guest policy.
|
|
91
|
+
def create_node(definition, record)
|
|
92
|
+
@progress.stage(45, 'Creating isolated container node...')
|
|
93
|
+
resource = @runtime.create_service(definition)
|
|
94
|
+
record['id'] = resource.fetch('id')
|
|
95
|
+
record['image_id'] = resource['image_id']
|
|
96
|
+
save
|
|
97
|
+
network_gateway.phase(record, bootstrap: true)
|
|
98
|
+
@runtime.start_service(resource)
|
|
99
|
+
resource = observed(record)
|
|
100
|
+
network_gateway.route(resource)
|
|
101
|
+
record['ssh_port'] = SSHEndpoint.port(resource) if record['ssh_transport'] == 'loopback'
|
|
102
|
+
save
|
|
103
|
+
apply_bootstrap(resource, record)
|
|
104
|
+
finish_network_bootstrap(record)
|
|
105
|
+
prepare_guest_environment(resource, record)
|
|
106
|
+
configure(resource, record)
|
|
107
|
+
resource
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def apply_bootstrap(resource, record)
|
|
111
|
+
apply_agent_bootstrap(resource, record)
|
|
112
|
+
ensure
|
|
113
|
+
@bootstrap_proxy.cleanup(@state) if @state
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def package_bootstrap(resource, record)
|
|
117
|
+
execute = ->(arguments) { @runtime.service_exec(resource, arguments, timeout: 300) }
|
|
118
|
+
requirements = package_requirements(record)
|
|
119
|
+
PackageBootstrap.new(config: context.configuration.dig('bootstrap', 'packages'),
|
|
120
|
+
os: record.fetch('os'), execute: execute,
|
|
121
|
+
copy: guest_copy(resource, execute),
|
|
122
|
+
rpm_options: requirements.rpm_options,
|
|
123
|
+
progress: @progress.method(:heartbeat))
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def package_requirements(record, agent_required: false)
|
|
127
|
+
::Empeira::VM::BootstrapRequirements.new(context: context, os: record.fetch('os'),
|
|
128
|
+
version: record.fetch('version'), agent_required: agent_required,
|
|
129
|
+
distribution_required: true,
|
|
130
|
+
architecture: record.fetch('architecture'))
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Node
|
|
5
|
+
# Runs the selected agent installation through container guest I/O.
|
|
6
|
+
module ContainerAgentBootstrap
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
# rubocop:disable-next Metrics/AbcSize -- The ordered guest transaction spans packages, APT and agent installation.
|
|
10
|
+
def apply_agent_bootstrap(resource, record)
|
|
11
|
+
execute = ->(arguments) { @runtime.service_exec(resource, arguments, timeout: 300) }
|
|
12
|
+
requirements = package_requirements(record, agent_required: true)
|
|
13
|
+
@bootstrap_proxy.start(@state, requirements, source: record.dig('definition', 'ip'))
|
|
14
|
+
family = PackageBootstrap::FAMILIES.fetch(record.fetch('os'))
|
|
15
|
+
configuration = family == 'debian' ? AptConfiguration : RpmConfiguration
|
|
16
|
+
configuration.new(os: record.fetch('os'), execute: execute).preserve do
|
|
17
|
+
packages = package_bootstrap(resource, record)
|
|
18
|
+
packages.run(proxy_url: @bootstrap_proxy.url) if packages.required?
|
|
19
|
+
install_agent(resource, record, requirements, execute)
|
|
20
|
+
end
|
|
21
|
+
ContainerBootstrap.new(runtime: @runtime).apply(resource: resource,
|
|
22
|
+
bootstrap: Bootstrap.new(provider: 'container'))
|
|
23
|
+
record['empeira_fact'] = ExternalFact.fingerprint('container')
|
|
24
|
+
save
|
|
25
|
+
PackageSources.verify!(execute: execute)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def install_agent(resource, _record, requirements, execute)
|
|
29
|
+
installer = AgentInstaller.build(context: context, runtime: @runtime, requirements: requirements,
|
|
30
|
+
execute: execute, copy: guest_copy(resource, execute), progress: @progress)
|
|
31
|
+
installer.install(proxy_url: @bootstrap_proxy.url)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def guest_copy(resource, execute)
|
|
35
|
+
lambda do |source, destination, mode|
|
|
36
|
+
@runtime.copy_to(resource, source, destination)
|
|
37
|
+
raise Error, 'Cannot set agent file permissions' unless
|
|
38
|
+
execute.call(['chmod', mode, destination]).success?
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Node
|
|
5
|
+
# Containers use the already installed agent Ruby runtime, never Cloud-Init.
|
|
6
|
+
class ContainerBootstrap
|
|
7
|
+
INSTALL = <<~RUBY
|
|
8
|
+
require 'fileutils'
|
|
9
|
+
path, content, mode = ARGV
|
|
10
|
+
FileUtils.mkdir_p(File.dirname(path), mode: 0755)
|
|
11
|
+
File.write(path, content, mode: 'w', perm: mode.to_i(8))
|
|
12
|
+
File.chmod(mode.to_i(8), path)
|
|
13
|
+
RUBY
|
|
14
|
+
VERIFY = <<~RUBY
|
|
15
|
+
path, content, mode = ARGV
|
|
16
|
+
exit(File.binread(path) == content && (File.stat(path).mode & 0777) == mode.to_i(8) ? 0 : 1)
|
|
17
|
+
RUBY
|
|
18
|
+
|
|
19
|
+
def initialize(runtime:)
|
|
20
|
+
@runtime = runtime
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def apply(resource:, bootstrap:)
|
|
24
|
+
bootstrap.files.each do |file|
|
|
25
|
+
run!(resource, INSTALL, file, 'install')
|
|
26
|
+
run!(resource, VERIFY, file, 'verify')
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# rubocop:disable-next Naming/PredicateMethod -- Reconciliation mutates owned guest state and reports changes.
|
|
31
|
+
def reconcile(resource:, bootstrap:)
|
|
32
|
+
bootstrap.files.map do |file|
|
|
33
|
+
arguments = [Certificates::RUBY, '-e', VERIFY, file.path, file.content, format('%04o', file.mode)]
|
|
34
|
+
next false if @runtime.service_exec(resource, arguments).success?
|
|
35
|
+
|
|
36
|
+
run!(resource, INSTALL, file, 'install')
|
|
37
|
+
run!(resource, VERIFY, file, 'verify')
|
|
38
|
+
true
|
|
39
|
+
end.any?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def run!(resource, code, file, action)
|
|
45
|
+
arguments = [Certificates::RUBY, '-e', code, file.path, file.content, format('%04o', file.mode)]
|
|
46
|
+
return if @runtime.service_exec(resource, arguments).success?
|
|
47
|
+
|
|
48
|
+
raise Error, "Cannot #{action} required Empeira node bootstrap file #{file.path}; Puppet was not run"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Node
|
|
5
|
+
module ContainerGuest
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def reconcile_guest(resource, record)
|
|
9
|
+
fact_changed = reconcile_provider_fact(resource, record)
|
|
10
|
+
environment_changed = prepare_guest_environment(resource, record)
|
|
11
|
+
reconcile_command_mocks(resource, record) || environment_changed || fact_changed
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# rubocop:disable-next Naming/PredicateMethod -- Reconciliation mutates owned guest state and reports changes.
|
|
15
|
+
def reconcile_provider_fact(resource, record)
|
|
16
|
+
digest = ExternalFact.fingerprint('container')
|
|
17
|
+
return false if record['empeira_fact'] == digest
|
|
18
|
+
|
|
19
|
+
ContainerBootstrap.new(runtime: @runtime).reconcile(resource: resource,
|
|
20
|
+
bootstrap: Bootstrap.new(provider: 'container'))
|
|
21
|
+
record['empeira_fact'] = digest
|
|
22
|
+
save
|
|
23
|
+
true
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def prepare_guest_environment(resource, record)
|
|
27
|
+
proxy_changed = reconcile_runtime_proxy(resource, record)
|
|
28
|
+
reconcile_interactive_tools(resource, record) || proxy_changed
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def reconcile_interactive_tools(resource, record)
|
|
32
|
+
InteractiveTools.new(record: record, persist: method(:save),
|
|
33
|
+
execute: ->(arguments) { @runtime.service_exec(resource, arguments) }).reconcile
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def reconcile_runtime_proxy(resource, record)
|
|
37
|
+
RuntimeProxy.new(context: context, record: record, persist: method(:save),
|
|
38
|
+
execute: ->(arguments) { @runtime.service_exec(resource, arguments) }).reconcile
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Node
|
|
5
|
+
# rubocop:disable-next Metrics/ModuleLength -- Provider lifecycle operations share one ownership boundary.
|
|
6
|
+
module ContainerLifecycle
|
|
7
|
+
def start(name:)
|
|
8
|
+
mutate do
|
|
9
|
+
record = fetch(name)
|
|
10
|
+
require_provisioned!(record)
|
|
11
|
+
resource = observed(record)
|
|
12
|
+
raise Error, 'Node is missing; destroy its reservation before recreating it' unless resource
|
|
13
|
+
|
|
14
|
+
ready_server
|
|
15
|
+
changed = resource['state'] != 'running'
|
|
16
|
+
@runtime.start_service(resource) if changed
|
|
17
|
+
network_gateway.route(observed(record))
|
|
18
|
+
refresh_dns
|
|
19
|
+
changed = reconcile_guest(resource, record) || changed
|
|
20
|
+
lifecycle_result(record['hostname'], :running, changed: changed)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def stop(name:)
|
|
25
|
+
mutate do
|
|
26
|
+
resource = observed(fetch(name))
|
|
27
|
+
raise Error, 'Node is missing; inspect node list' unless resource
|
|
28
|
+
|
|
29
|
+
changed = resource['state'] == 'running'
|
|
30
|
+
@runtime.stop_service(resource) if changed
|
|
31
|
+
lifecycle_result(name.downcase, :stopped, changed: changed)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def destroy(name:)
|
|
36
|
+
Infrastructure::Cleanup.new(context: context, runner: @runner, runtime: @runtime,
|
|
37
|
+
progress: @progress).node(name)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def puppet(name:)
|
|
41
|
+
mutate(availability: false) do
|
|
42
|
+
record = fetch(name)
|
|
43
|
+
require_provisioned!(record)
|
|
44
|
+
resource = running(record)
|
|
45
|
+
ready_network!
|
|
46
|
+
@progress.stage(30, 'Running Puppet agent...')
|
|
47
|
+
agent_run(resource, record)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def shell(name:)
|
|
52
|
+
mutate do
|
|
53
|
+
resource = running(fetch(name))
|
|
54
|
+
raise Error, 'Node shell requires a local terminal' unless $stdin.tty? && $stdout.tty?
|
|
55
|
+
|
|
56
|
+
arguments = RuntimeProxy.command(context, ['env', 'HISTFILE=/dev/null', '/bin/bash', '-i'])
|
|
57
|
+
@runtime.stream_service(resource, arguments, interactive: true, user: '0')
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def console(name:)
|
|
62
|
+
raise Error, "Container node #{name} has no serial VM console; use node shell or node exec"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def exec(name:, arguments:, timeout: nil, output: $stdout, error: $stderr)
|
|
66
|
+
DiagnosticCommand.validate!(arguments, timeout)
|
|
67
|
+
mutate do
|
|
68
|
+
Execution::Terminal.with_interrupts do
|
|
69
|
+
@progress.streaming do
|
|
70
|
+
@runtime.diagnostic_exec(running(fetch(name)), arguments, timeout: timeout, output: output, error: error)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# rubocop:disable-next Metrics/AbcSize, Metrics/MethodLength -- Verify the owned endpoint before selecting a guest tunnel.
|
|
77
|
+
def ssh(name:, user: nil, identity: nil, port: nil)
|
|
78
|
+
mutate do
|
|
79
|
+
port = Configuration::SSHPreferences.port!(port || 22)
|
|
80
|
+
record = fetch(name)
|
|
81
|
+
resource = running(record)
|
|
82
|
+
unless record['ssh_host'] && record['ssh_port']
|
|
83
|
+
raise Error, 'Node has no managed SSH endpoint; destroy and recreate it with the current Empeira image'
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
proxy = if port != 22
|
|
87
|
+
@runtime.ssh_proxy_command(resource, port: port)
|
|
88
|
+
elsif record['ssh_transport'] == 'tunnel'
|
|
89
|
+
@runtime.ssh_proxy_command(resource)
|
|
90
|
+
end
|
|
91
|
+
record = record.merge('ssh_port' => port) if port != 22
|
|
92
|
+
UserSSH.new(runner: @runner, credentials: ssh_credentials(record.fetch('hostname')),
|
|
93
|
+
proxy_command: proxy, home: context.locations.home).session(
|
|
94
|
+
record, user: user, identity: identity
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def logs(name:)
|
|
100
|
+
load_state
|
|
101
|
+
check_runtime!
|
|
102
|
+
resource = observed(fetch(name))
|
|
103
|
+
raise Error, 'Node container is missing' unless resource
|
|
104
|
+
|
|
105
|
+
@runtime.stream_service(resource, [], logs: true)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
def require_provisioned!(record)
|
|
111
|
+
return if record['provisioned']
|
|
112
|
+
|
|
113
|
+
raise Error, 'Node bootstrap is incomplete; inspect logs or shell, then destroy and recreate the node'
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def remove_node(name, certificates: true)
|
|
117
|
+
record = @nodes[name]
|
|
118
|
+
return unless record
|
|
119
|
+
|
|
120
|
+
observed(record)
|
|
121
|
+
if certificates && record['certificate_key']
|
|
122
|
+
Certificates.new(runtime: @runtime, server: ready_server).clean(record)
|
|
123
|
+
end
|
|
124
|
+
@runtime.remove_service(definition(record), expected_id: record['id'])
|
|
125
|
+
ssh_credentials(name).cleanup
|
|
126
|
+
@nodes.delete(name)
|
|
127
|
+
save
|
|
128
|
+
true
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def agent_run(resource, record)
|
|
132
|
+
require_provisioned!(record)
|
|
133
|
+
reconcile_provider_fact(resource, record)
|
|
134
|
+
result = @progress.streaming do
|
|
135
|
+
@runtime.stream_service(resource, RuntimeProxy.command(context, PuppetCommand.arguments))
|
|
136
|
+
end
|
|
137
|
+
record['last_puppet_exit'] = result.exit_status
|
|
138
|
+
save
|
|
139
|
+
return result if [0, 2].include?(result.exit_status)
|
|
140
|
+
|
|
141
|
+
raise Error, "Puppet agent failed (exit #{result.exit_status}); node retained for shell/logs and another run"
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|