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,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Node
|
|
5
|
+
# rubocop:disable-next Metrics/ModuleLength -- Container preparation shares its ownership transaction.
|
|
6
|
+
module ContainerPreparation
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def network_gateway
|
|
10
|
+
Network::Gateway.new(context: context, runtime: @runtime, state: @state)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def finish_network_bootstrap(record)
|
|
14
|
+
@bootstrap_proxy.cleanup(@state)
|
|
15
|
+
record['network_phase'] = 'runtime'
|
|
16
|
+
save
|
|
17
|
+
network_gateway.phase(record, bootstrap: false)
|
|
18
|
+
refresh_dns
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def ready_server
|
|
22
|
+
raise Error, 'Control plane is down; run empeira up first' unless @state&.dig('resources', 'network', 'id')
|
|
23
|
+
|
|
24
|
+
ready_network!
|
|
25
|
+
plan = ControlPlane::Plan.new(context: context)
|
|
26
|
+
expected = @state.dig('control_plane', 'services', 'server', 'id')
|
|
27
|
+
server = @runtime.inspect_service(plan.definitions.fetch('server'), expected_id: expected)
|
|
28
|
+
unless current_server?(server, plan) &&
|
|
29
|
+
ControlPlane::Health.new(runtime: @runtime, plan: plan).ready?('server', 'server' => server)
|
|
30
|
+
raise Error, 'Configuration server is not ready; run empeira up first'
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
server
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def ready_network!
|
|
37
|
+
definition = Infrastructure::Definition.new(context: context).network
|
|
38
|
+
expected = @state&.dig('resources', 'network', 'id')
|
|
39
|
+
raise Error, 'Control plane is down; run empeira up first' unless expected
|
|
40
|
+
|
|
41
|
+
@runtime.verify_isolated_network(definition, expected_id: expected)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def current_server?(server, plan)
|
|
45
|
+
server&.dig('labels', 'io.empeira.definition') == plan.definitions.fetch('server').fingerprint
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def reserve(request)
|
|
49
|
+
reject_name!(request.hostname.downcase)
|
|
50
|
+
plan = ControlPlane::Plan.new(context: context)
|
|
51
|
+
|
|
52
|
+
image = Image.new(config: context.configuration, request: request, architecture: @runtime.architecture)
|
|
53
|
+
definition = node_definition(request, image, plan)
|
|
54
|
+
record = node_record(request, definition, image)
|
|
55
|
+
@nodes[record.fetch('hostname')] = record
|
|
56
|
+
save
|
|
57
|
+
[record, definition, image]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def node_record(request, definition, image)
|
|
61
|
+
request.to_h.transform_keys(&:to_s).merge(
|
|
62
|
+
'hostname' => request.hostname.downcase, 'runtime' => context.container_engine,
|
|
63
|
+
'architecture' => @runtime.architecture, **ssh_record,
|
|
64
|
+
'image' => image.reference, 'id' => nil, 'network_phase' => 'bootstrap', 'provisioned' => false,
|
|
65
|
+
'created_at' => Time.now.utc.iso8601,
|
|
66
|
+
'definition' => definition.options
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def ssh_record
|
|
71
|
+
publication = @runtime.node_ssh_publication?
|
|
72
|
+
{ 'ssh_host' => '127.0.0.1', 'ssh_port' => publication ? nil : 22,
|
|
73
|
+
'ssh_transport' => publication ? 'loopback' : 'tunnel' }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def reject_name!(hostname)
|
|
77
|
+
reserved = Network::Naming.new.reserved(context.configuration)
|
|
78
|
+
aliases = [hostname, hostname.delete_suffix('.empeira.internal')]
|
|
79
|
+
taken = @nodes.keys.any? { |key| aliases.include?(key.delete_suffix('.empeira.internal')) }
|
|
80
|
+
return unless aliases.intersect?(reserved) || taken
|
|
81
|
+
|
|
82
|
+
raise Providers::AlreadyExists, 'Hostname is reserved or already exists in this workspace'
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def node_definition(request, image, plan)
|
|
86
|
+
init = node_init
|
|
87
|
+
Definition.new(hostname: request.hostname.downcase, workspace: context.workspace,
|
|
88
|
+
network: plan.network, dns: dns_address(plan), image: image.reference,
|
|
89
|
+
memory: request.memory, cpus: request.cpus, init: init,
|
|
90
|
+
**peer_options,
|
|
91
|
+
command: init == 'systemd' ? ['/sbin/init'] : %w[/usr/sbin/sshd -D -e],
|
|
92
|
+
**ssh_options(request),
|
|
93
|
+
environment: plan.node_proxy_environment)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def peer_options
|
|
97
|
+
layout = Network::Peer::Layout.new(@state.fetch('peer_network').fetch('subnet'))
|
|
98
|
+
{ ip: layout.container_lease(@nodes), mac_address: "02:#{SecureRandom.hex(5).scan(/../).join(':')}",
|
|
99
|
+
sysctls: { 'net.ipv6.conf.all.disable_ipv6' => '1', 'net.ipv6.conf.default.disable_ipv6' => '1' } }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def node_init
|
|
103
|
+
init = context.configuration.dig('node_defaults', 'init')
|
|
104
|
+
@runtime.require_systemd! if init == 'systemd'
|
|
105
|
+
init
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def ssh_options(request)
|
|
109
|
+
ssh_credentials(request.hostname.downcase).prepare_hosts
|
|
110
|
+
{ entrypoint: '/usr/local/libexec/empeira-node-start',
|
|
111
|
+
ports: @runtime.node_ssh_publication? ? ['127.0.0.1::22/tcp'] : [],
|
|
112
|
+
mounts: [] }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def dns_address(plan)
|
|
116
|
+
dns = @runtime.inspect_service(plan.definitions.fetch('dns'),
|
|
117
|
+
expected_id: @state.dig('control_plane', 'services', 'dns', 'id'))
|
|
118
|
+
address = dns&.dig('networks', plan.network, 'IPAddress')
|
|
119
|
+
raise Error, 'Empeira DNS is unavailable; run empeira up first' if address.to_s.empty?
|
|
120
|
+
|
|
121
|
+
address
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def ssh_credentials(hostname)
|
|
125
|
+
SSHCredentials.new(context: context, runner: @runner, provider: 'container', hostname: hostname)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def configure(resource, record)
|
|
129
|
+
{ 'certname' => record.fetch('hostname'), 'server' => 'server.empeira.internal',
|
|
130
|
+
'environment' => context.configuration.dig('server', 'environment') }.each do |key, value|
|
|
131
|
+
result = @runtime.service_exec(resource,
|
|
132
|
+
[Certificates::PUPPET, 'config', 'set', key, value, '--section', 'main'])
|
|
133
|
+
raise Error, 'Node agent configuration failed; node retained for diagnosis' unless result.success?
|
|
134
|
+
end
|
|
135
|
+
refresh_dns
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def refresh_dns
|
|
139
|
+
plan = ControlPlane::Plan.new(context: context)
|
|
140
|
+
ControlPlane::Discovery.new(plan: plan, runtime: @runtime, state: @state).refresh
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Node
|
|
5
|
+
module ContainerState
|
|
6
|
+
def list
|
|
7
|
+
load_state
|
|
8
|
+
records = container_records
|
|
9
|
+
return [] if records.empty?
|
|
10
|
+
|
|
11
|
+
check_runtime!
|
|
12
|
+
definitions = records.transform_values { |record| definition(record) }
|
|
13
|
+
resources = @runtime.inspect_services(definitions, expected_ids: records.transform_values do |record|
|
|
14
|
+
record['id']
|
|
15
|
+
end)
|
|
16
|
+
records.map { |name, record| inventory_row(record, resources[name]) }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def inspect_resource(name:)
|
|
20
|
+
load_state
|
|
21
|
+
record = @nodes[name.downcase]
|
|
22
|
+
return unless record
|
|
23
|
+
|
|
24
|
+
check_runtime!
|
|
25
|
+
resource = observed(record)
|
|
26
|
+
return unless resource
|
|
27
|
+
|
|
28
|
+
state = record['provisioned'] && resource['state'] == 'running' ? :running : :stopped
|
|
29
|
+
Providers::Resource.new(name: record.fetch('hostname'), owner: context.workspace.id, state: state)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def inventory_row(record, resource)
|
|
35
|
+
verify_network!(resource, record) if resource
|
|
36
|
+
record.slice('hostname', 'provider', 'runtime', 'os', 'version', 'last_puppet_exit').merge(
|
|
37
|
+
'state' => state_of(record, resource)
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def container_records
|
|
42
|
+
@nodes.select { |_, record| record['provider'] == 'container' }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def lifecycle_result(name, state, changed:)
|
|
46
|
+
resource = Providers::Resource.new(name: name, owner: context.workspace.id, state: state)
|
|
47
|
+
Providers::Result.new(resource: resource, changed: changed)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def mutate(availability: true)
|
|
51
|
+
@build_info.require_compatible!(context.configuration.dig('requirements', 'empeira'))
|
|
52
|
+
@store.with_lock do
|
|
53
|
+
load_state
|
|
54
|
+
check_runtime!(availability: availability)
|
|
55
|
+
yield
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def load_state
|
|
60
|
+
@state = @store.load
|
|
61
|
+
@nodes = @state&.fetch('nodes', {}) || {}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def save
|
|
65
|
+
@state['nodes'] = @nodes
|
|
66
|
+
@store.write(@state)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def check_runtime!(availability: true)
|
|
70
|
+
if @state && @state.fetch('runtime') != context.container_engine
|
|
71
|
+
raise Error, 'Node runtime differs from workspace ownership; restore runtime.container_engine'
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
@runtime.check_available! if availability
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def fetch(name)
|
|
78
|
+
@nodes.fetch(name.downcase) { raise Providers::NotFound, 'Node does not exist in this workspace' }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def definition(record)
|
|
82
|
+
Definition.new(hostname: record.fetch('hostname'), workspace: context.workspace,
|
|
83
|
+
**record.fetch('definition').except('hostname').transform_keys(&:to_sym))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def observed(record)
|
|
87
|
+
resource = @runtime.inspect_service(definition(record), expected_id: record['id'])
|
|
88
|
+
verify_network!(resource, record) if resource
|
|
89
|
+
resource
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def verify_network!(resource, record)
|
|
93
|
+
network = Network::Definition.new(workspace: context.workspace, policy: Network::Policy.new).backend_name
|
|
94
|
+
return if resource.fetch('networks').keys == [network] && SSHEndpoint.valid?(resource, record)
|
|
95
|
+
|
|
96
|
+
raise Providers::OwnershipError, 'Node has unexpected network membership or published ports'
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def running(record)
|
|
100
|
+
resource = observed(record)
|
|
101
|
+
raise Error, 'Node is missing; destroy its reservation before recreating it' unless resource
|
|
102
|
+
unless resource['state'] == 'running'
|
|
103
|
+
raise Error,
|
|
104
|
+
"Node #{record.fetch('hostname')} is stopped; " \
|
|
105
|
+
"start it with: empeira node start #{record.fetch('hostname')}"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
resource
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def state_of(record, resource)
|
|
112
|
+
return 'missing' unless resource
|
|
113
|
+
return 'incomplete' unless record['provisioned']
|
|
114
|
+
return 'stale' unless resource.dig('labels', 'io.empeira.definition') == definition(record).fingerprint
|
|
115
|
+
return 'stale' unless Image.new(config: context.configuration, request: request_for(record),
|
|
116
|
+
architecture: record['architecture']).reference == record['image']
|
|
117
|
+
|
|
118
|
+
resource['state'] == 'exited' ? 'stopped' : resource['state']
|
|
119
|
+
rescue ConfigurationError
|
|
120
|
+
'stale'
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def request_for(record)
|
|
124
|
+
RunRequest.new(**record.slice('hostname', 'provider', 'os', 'version', 'memory', 'cpus')
|
|
125
|
+
.transform_keys(&:to_sym))
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Node
|
|
5
|
+
class Definition < Services::Definition
|
|
6
|
+
attr_reader :hostname
|
|
7
|
+
|
|
8
|
+
def initialize(hostname:, workspace:, **)
|
|
9
|
+
@hostname = hostname
|
|
10
|
+
super(key: "node-#{Digest::SHA256.hexdigest(hostname)[0, 24]}", workspace: workspace,
|
|
11
|
+
hostname: hostname, **)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def ownership_labels
|
|
15
|
+
super.merge('io.empeira.purpose' => 'node', 'io.empeira.hostname' => hostname,
|
|
16
|
+
'io.empeira.provider' => 'container')
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Node
|
|
5
|
+
module DiagnosticCommand
|
|
6
|
+
def self.validate!(arguments, timeout)
|
|
7
|
+
raise ConfigurationError, 'Provide a guest command and arguments after --' unless valid_arguments?(arguments)
|
|
8
|
+
return if timeout.nil? || (timeout.is_a?(Numeric) && timeout.finite? && timeout.positive? && timeout <= 86_400)
|
|
9
|
+
|
|
10
|
+
raise ConfigurationError, 'Command timeout must be positive and at most 86400 seconds'
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.valid_arguments?(arguments)
|
|
14
|
+
arguments.is_a?(Array) && !arguments.empty? && !arguments.first.to_s.empty? &&
|
|
15
|
+
arguments.all? { |argument| argument.is_a?(String) && !argument.include?("\0") }
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rubocop:disable Style/FormatStringToken -- RPM and DNF use their native query-format tokens.
|
|
4
|
+
|
|
5
|
+
module Empeira
|
|
6
|
+
module Node
|
|
7
|
+
# DNF metadata is queried against the isolated source directory only.
|
|
8
|
+
# rubocop:disable-next Metrics/ClassLength -- DNF and RPM share one isolated source and its temporary trusted keys.
|
|
9
|
+
class DnfAgentRepository < AgentRepository
|
|
10
|
+
REPO_PATH = '/var/tmp/empeira-agent-repos/agent.repo'
|
|
11
|
+
KEY_PATH = '/var/tmp/empeira-agent-rpm-key.gpg'
|
|
12
|
+
CONFIG_PATH = '/var/tmp/empeira-agent-dnf.conf'
|
|
13
|
+
PACKAGE_PATH = '/var/tmp/empeira-agent-package.rpm'
|
|
14
|
+
QUERY_FORMAT = '%{name}|%{epoch}|%{version}|%{release}|%{arch}|%{reponame}'
|
|
15
|
+
|
|
16
|
+
def resolve
|
|
17
|
+
@release_package = nil
|
|
18
|
+
@release_sources = {}
|
|
19
|
+
@repository_ids = []
|
|
20
|
+
install_source
|
|
21
|
+
result = native!([*dnf_command, 'repoquery', "--archlist=#{@target.native_architecture},noarch",
|
|
22
|
+
'--qf', QUERY_FORMAT, @package], 'DNF package resolution')
|
|
23
|
+
candidates = result.stdout.lines.filter_map { |line| dnf_candidate(line) }
|
|
24
|
+
candidate = Agent::Versions.select(candidates, @version, suffix: @source['suffix'])
|
|
25
|
+
candidate.merge('url' => package_url(candidate))
|
|
26
|
+
ensure
|
|
27
|
+
remove_release_package if @release_package
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def prepare_keys
|
|
31
|
+
key = super
|
|
32
|
+
execute!(['rpmkeys', '--import', key], 'RPM signing key import') if key
|
|
33
|
+
key
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def inspect_package(path)
|
|
37
|
+
@copy.call(path, PACKAGE_PATH, '0600')
|
|
38
|
+
result = execute!(['rpm', '-qp', '--qf', '%{NAME}|%{EPOCHNUM}:%{VERSION}-%{RELEASE}|%{ARCH}', PACKAGE_PATH],
|
|
39
|
+
'RPM package inspection')
|
|
40
|
+
name, version, architecture = result.stdout.strip.split('|')
|
|
41
|
+
version = version.delete_prefix('0:') if version
|
|
42
|
+
metadata = validate_metadata(name, version, architecture)
|
|
43
|
+
verify_signature unless @source['verify_signatures'] == false
|
|
44
|
+
metadata
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def install_source
|
|
50
|
+
execute!(['mkdir', '-p', File.dirname(REPO_PATH)], 'source preparation')
|
|
51
|
+
if @source['url'].end_with?('.rpm')
|
|
52
|
+
install_release(@source)
|
|
53
|
+
collect_release_sources
|
|
54
|
+
prepare_keys
|
|
55
|
+
else
|
|
56
|
+
@repository_ids = ['empeira-agent']
|
|
57
|
+
prepare_keys
|
|
58
|
+
copy_text(repo_content, REPO_PATH, mode: '0600')
|
|
59
|
+
end
|
|
60
|
+
copy_text("[main]\ngpgcheck=1\nsslverify=1\nlogfilelevel=0\nreposdir=#{File.dirname(REPO_PATH)}\n",
|
|
61
|
+
CONFIG_PATH, mode: '0600')
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def install_release(artifact)
|
|
65
|
+
path = download_artifact(artifact, 'release.rpm')
|
|
66
|
+
destination = '/var/tmp/empeira-agent-release.rpm'
|
|
67
|
+
@copy.call(path, destination, '0600')
|
|
68
|
+
name = execute!(['rpm', '-qp', '--qf', '%{NAME}', destination], 'release metadata').stdout.strip
|
|
69
|
+
require_absent_release!(name)
|
|
70
|
+
@release_package = name
|
|
71
|
+
execute!(['rpm', '-i', destination], 'release package installation')
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def collect_release_sources
|
|
75
|
+
files = execute!(['rpm', '-ql', @release_package], 'release source discovery').stdout.lines.map(&:strip)
|
|
76
|
+
files = files.select { |file| file.start_with?('/etc/yum.repos.d/') && file.end_with?('.repo') }
|
|
77
|
+
raise Error, 'Agent release package provides no native DNF source' if files.empty?
|
|
78
|
+
|
|
79
|
+
files.each_with_index do |file, index|
|
|
80
|
+
content = execute!(['cat', file], 'release repository configuration').stdout
|
|
81
|
+
import_release_keys(content)
|
|
82
|
+
store_release_source(content, index)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def store_release_source(content, index)
|
|
87
|
+
path = "#{File.dirname(REPO_PATH)}/#{index}.repo"
|
|
88
|
+
scoped = Agent::ReleaseSources.dnf(content, target: @target, source: @source, package: @package)
|
|
89
|
+
@repository_ids.concat(scoped.scan(/^\[([^\]]+)\]/).flatten)
|
|
90
|
+
@release_sources[path] = scoped
|
|
91
|
+
copy_text(scoped, path, mode: '0600')
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def import_release_keys(content)
|
|
95
|
+
content.scan(/^\s*gpgkey\s*=\s*(.+)$/).flatten.flat_map(&:split).each_with_index do |url, index|
|
|
96
|
+
path = release_key(url, index)
|
|
97
|
+
@public_keys << Base64.strict_encode64(File.binread(path))
|
|
98
|
+
guest_path = "#{KEY_PATH}-#{index}"
|
|
99
|
+
@copy.call(path, guest_path, '0600')
|
|
100
|
+
execute!(['rpmkeys', '--import', guest_path], 'RPM signing key import')
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def release_key(url, index)
|
|
105
|
+
path = File.join(@directory, "release-key-#{index}")
|
|
106
|
+
if url.start_with?('file:///')
|
|
107
|
+
key = execute!(['cat', URI(url).path], 'release signing key').stdout
|
|
108
|
+
File.write(path, key, perm: 0o600)
|
|
109
|
+
else
|
|
110
|
+
@download.fetch(url, path, operation: 'Download agent release signing key')
|
|
111
|
+
end
|
|
112
|
+
path
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def repo_content
|
|
116
|
+
credentials = @authentication.credentials
|
|
117
|
+
lines = ['[empeira-agent]', 'name=Empeira temporary agent source', "baseurl=#{@source.fetch('url')}",
|
|
118
|
+
'enabled=1', "gpgcheck=#{@source['verify_signatures'] == false ? 0 : 1}",
|
|
119
|
+
'sslverify=1', 'skip_if_unavailable=0', "includepkgs=#{@package}"]
|
|
120
|
+
lines.push("username=#{credentials.first}", "password=#{credentials.last}") if credentials
|
|
121
|
+
"#{lines.join("\n")}\n"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def install_auth
|
|
125
|
+
if @release_package
|
|
126
|
+
@release_sources.each do |path, content|
|
|
127
|
+
copy_text(authenticated_release_content(content), path, mode: '0600')
|
|
128
|
+
end
|
|
129
|
+
return
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
copy_text(repo_content, REPO_PATH, mode: '0600')
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def authenticated_release_content(content)
|
|
136
|
+
credentials = @authentication.credentials
|
|
137
|
+
return content unless credentials
|
|
138
|
+
|
|
139
|
+
content.gsub(/^(\s*baseurl\s*=\s*)(\S+)$/) do
|
|
140
|
+
line = Regexp.last_match(0)
|
|
141
|
+
url = @target.expand(Regexp.last_match(2))
|
|
142
|
+
if @authentication.credentials(url)
|
|
143
|
+
"#{line}\nusername=#{credentials.first}\npassword=#{credentials.last}"
|
|
144
|
+
else
|
|
145
|
+
line
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def dnf_command
|
|
151
|
+
# --quiet also hides HTTP status details needed for the one authentication retry.
|
|
152
|
+
['dnf', "--config=#{CONFIG_PATH}", "--releasever=#{@target.release}"]
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def package_url(candidate)
|
|
156
|
+
spec = "#{@package}-#{candidate.fetch('version')}.#{candidate.fetch('architecture')}"
|
|
157
|
+
urls = native!([*dnf_command, 'repoquery', "--repoid=#{candidate.fetch('repository')}", '--location', spec],
|
|
158
|
+
'DNF artifact location').stdout.lines
|
|
159
|
+
.map(&:strip).select { |line| line.start_with?('https://') }.uniq
|
|
160
|
+
raise Error, 'Selected agent source did not provide one HTTPS package location' unless urls.size == 1
|
|
161
|
+
|
|
162
|
+
Configuration::AgentSchema.https_url!(urls.first, 'agent resolved package URL')
|
|
163
|
+
urls.first
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def dnf_candidate(line)
|
|
167
|
+
name, epoch, version, release, architecture, repository = line.strip.split('|')
|
|
168
|
+
return unless name == @package && [@target.native_architecture, 'noarch'].include?(architecture) &&
|
|
169
|
+
@repository_ids.include?(repository)
|
|
170
|
+
|
|
171
|
+
native = "#{epoch}:#{version}-#{release}".delete_prefix('0:')
|
|
172
|
+
{ 'version' => native, 'architecture' => architecture, 'repository' => repository }
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def validate_metadata(name, version, architecture)
|
|
176
|
+
unless name == @package && [@target.native_architecture, 'noarch'].include?(architecture) &&
|
|
177
|
+
Agent::Versions.match?(version.to_s, @version, suffix: @source['suffix'])
|
|
178
|
+
raise Error, 'Agent package name, native version or architecture differs from the requested target'
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
{ 'version' => version, 'architecture' => architecture }
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def verify_signature
|
|
185
|
+
result = execute!(['rpmkeys', '--checksig', '--verbose', PACKAGE_PATH], 'RPM package signature verification')
|
|
186
|
+
return if result.stdout.match?(/Signature[^\n]*:\s*OK/i) && !result.stdout.match?(/NOKEY|NOT OK/i)
|
|
187
|
+
|
|
188
|
+
raise Error, 'Agent RPM package has no verifiable trusted signature'
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def release_inventory_arguments
|
|
192
|
+
['rpm', '-qa', '--qf', "%{NAME}\n"]
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def release_removal_arguments
|
|
196
|
+
['rpm', '-e', '--', @release_package]
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# rubocop:enable Style/FormatStringToken
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Node
|
|
5
|
+
# Logical provider and the fixed native Hiera selector are present before the first catalog.
|
|
6
|
+
module ExternalFact
|
|
7
|
+
PATH = '/etc/puppetlabs/facter/facts.d/empeira.yaml'
|
|
8
|
+
PROVIDERS = %w[container vm].freeze
|
|
9
|
+
|
|
10
|
+
def self.content(provider)
|
|
11
|
+
raise ConfigurationError, 'Unknown Empeira node provider' unless PROVIDERS.include?(provider)
|
|
12
|
+
|
|
13
|
+
"empeira:\n provider: #{provider}\n hierarchy: empeira\n"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.fingerprint(provider)
|
|
17
|
+
Digest::SHA256.hexdigest(content(provider))
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Node
|
|
5
|
+
class Image
|
|
6
|
+
attr_reader :reference, :recipe, :build_files
|
|
7
|
+
|
|
8
|
+
def initialize(config:, request:, architecture:)
|
|
9
|
+
image = config.dig('images', 'nodes', request.os, request.version)
|
|
10
|
+
raise ConfigurationError, 'node_defaults.os/version must select a configured images.nodes entry' unless image
|
|
11
|
+
unless image.fetch('architectures', []).include?(architecture.to_s)
|
|
12
|
+
raise Error, 'Node image does not support runtime architecture; configure images.nodes without emulation'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
@build_files = {}
|
|
16
|
+
@reference = Images::Configuration.reference(image, registry: config.dig('images', 'registry'))
|
|
17
|
+
return if image['reference'] || !image['build']
|
|
18
|
+
|
|
19
|
+
prepare_recipe(config, image)
|
|
20
|
+
@reference = Images::Configuration.local_image(recipe, purpose: "node-#{architecture}")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def prepare_recipe(config, image)
|
|
26
|
+
root = Pathname(Images::Configuration::ROOT).join('nodes/ssh')
|
|
27
|
+
@build_files = { 'node-start' => root.join('start').read, 'sshd_config' => root.join('sshd_config').read,
|
|
28
|
+
'node-connect' => root.join('../../network/ssh_connect.rb').read }
|
|
29
|
+
@recipe = build_recipe(config, image) + "\n# SSH resources: #{Infrastructure::Definition.fingerprint(@build_files)}\n"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def build_recipe(config, image)
|
|
33
|
+
template = Images::Configuration.recipe(image, registry: config.dig('images', 'registry'))
|
|
34
|
+
template.gsub('@BASE_IMAGE@', reference)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Node
|
|
5
|
+
# Agent tools are exposed through owned interactive OS startup fragments.
|
|
6
|
+
class InteractiveTools
|
|
7
|
+
def initialize(record:, execute:, persist:)
|
|
8
|
+
@record = record
|
|
9
|
+
@execute = execute
|
|
10
|
+
@persist = persist
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.valid_inventory?(entry)
|
|
14
|
+
entry == { 'version' => 1 }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def reconcile
|
|
18
|
+
helper = Pathname(__dir__).join('../../../resources/nodes/interactive_tools.rb').read
|
|
19
|
+
@record['interactive_tools'] = { 'version' => 1 }
|
|
20
|
+
@persist.call
|
|
21
|
+
result = @execute.call([Certificates::RUBY, '-e', helper, JSON.generate('puppet' => Certificates::PUPPET)])
|
|
22
|
+
unless result.success?
|
|
23
|
+
details = Execution::Diagnostics.command(result, operation: 'Interactive agent tool PATH', tool: 'ruby')
|
|
24
|
+
raise Error, "Cannot configure interactive agent tools; node retained for diagnosis\n#{details}", cause: nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
JSON.parse(result.stdout).fetch('changed')
|
|
28
|
+
rescue JSON::ParserError, KeyError
|
|
29
|
+
raise Error, 'Cannot verify interactive agent tool PATH; node retained for diagnosis', cause: nil
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Node
|
|
5
|
+
class Interface < Providers::Lifecycle
|
|
6
|
+
attr_reader :backend
|
|
7
|
+
|
|
8
|
+
def initialize(backend:, **dependencies)
|
|
9
|
+
super(**dependencies)
|
|
10
|
+
@backend = backend
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def run(request)
|
|
14
|
+
super(name: request.hostname, request: request)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|