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,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Node
|
|
5
|
+
# rubocop:disable-next Metrics/ModuleLength -- Provider inventories share one explicit ownership envelope.
|
|
6
|
+
module Inventory
|
|
7
|
+
def self.valid?(nodes)
|
|
8
|
+
return false unless nodes.is_a?(Hash)
|
|
9
|
+
|
|
10
|
+
nodes.all? { |name, record| valid_record?(name, record) }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.valid_record?(name, record)
|
|
14
|
+
return false unless record.is_a?(Hash) && record['hostname'] == name && valid_name?(name)
|
|
15
|
+
return false unless valid_guest_settings?(record)
|
|
16
|
+
|
|
17
|
+
case record['provider']
|
|
18
|
+
when 'container' then valid_container?(record)
|
|
19
|
+
when 'vm' then valid_vm?(name, record)
|
|
20
|
+
else false
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.valid_guest_settings?(record)
|
|
25
|
+
CommandMocks.valid_inventory?(record.fetch('command_mocks', {})) &&
|
|
26
|
+
(!record.key?('runtime_proxy') || RuntimeProxy.valid_inventory?(record['runtime_proxy'])) &&
|
|
27
|
+
(!record.key?('interactive_tools') || InteractiveTools.valid_inventory?(record['interactive_tools'])) &&
|
|
28
|
+
valid_fact_revision?(record)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.valid_fact_revision?(record)
|
|
32
|
+
return true unless record.key?('empeira_fact')
|
|
33
|
+
|
|
34
|
+
record['empeira_fact'].is_a?(String) && record['empeira_fact'].match?(/\A[0-9a-f]{64}\z/)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.valid_container?(record)
|
|
38
|
+
Runtime.registry.names.include?(record['runtime']) &&
|
|
39
|
+
[true, false].include?(record['provisioned']) &&
|
|
40
|
+
valid_definition?(record) && valid_metadata?(record) && valid_ssh?(record)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.valid_ssh?(record)
|
|
44
|
+
return true unless record.key?('ssh_host') || record.key?('ssh_port')
|
|
45
|
+
|
|
46
|
+
record['ssh_host'] == '127.0.0.1' && %w[loopback tunnel].include?(record['ssh_transport']) &&
|
|
47
|
+
(record['ssh_port'].nil? || (record['ssh_port'].is_a?(Integer) && record['ssh_port'].between?(1, 65_535)))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.valid_vm?(name, record)
|
|
51
|
+
valid_vm_image?(record['base_image']) && valid_vm_process?(record) &&
|
|
52
|
+
valid_vm_network?(name, record) && valid_vm_metadata?(record) &&
|
|
53
|
+
valid_vm_management_layout?(record) &&
|
|
54
|
+
(!record.key?('network_interfaces') || ::Empeira::VM::Interfaces.valid_inventory?(record['network_interfaces']))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.valid_vm_management_layout?(record)
|
|
58
|
+
record['management_layout'] == ::Empeira::VM::Management::VERSION && valid_management_socket?(record)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.valid_management_socket?(record)
|
|
62
|
+
valid_socket_identity?(record['management_socket']) && valid_management_slots?(record)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# rubocop:disable-next Metrics/CyclomaticComplexity -- Match all slot identities and the original primary endpoint.
|
|
66
|
+
def self.valid_management_slots?(record)
|
|
67
|
+
return true unless record.key?('management_sockets')
|
|
68
|
+
|
|
69
|
+
slots = record['management_sockets']
|
|
70
|
+
slots.is_a?(Hash) && slots.keys.sort == (0...::Empeira::VM::Management::SLOTS).map(&:to_s) &&
|
|
71
|
+
slots.values.all? { |value| !value.nil? && valid_socket_identity?(value) } &&
|
|
72
|
+
slots['0'] == record['management_socket']
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def self.valid_socket_identity?(socket)
|
|
76
|
+
socket.nil? || (socket.is_a?(Hash) && socket.keys.sort == %w[device inode] &&
|
|
77
|
+
socket.values.all? { |value| value.is_a?(Integer) && value >= 0 })
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def self.valid_vm_image?(image)
|
|
81
|
+
return false unless image.is_a?(Hash)
|
|
82
|
+
|
|
83
|
+
required = %w[distribution version architecture source revision checksum]
|
|
84
|
+
required.all? { |key| image[key].is_a?(String) && !image[key].empty? } &&
|
|
85
|
+
image['source'].start_with?('https://') &&
|
|
86
|
+
image['checksum'].match?(/\A[0-9a-f]{64}\z/)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def self.valid_vm_process?(record)
|
|
90
|
+
record['engine'] == 'qemu' && %w[kvm hvf].include?(record['accelerator']) &&
|
|
91
|
+
%w[amd64 arm64].include?(record['architecture']) &&
|
|
92
|
+
valid_vm_resources?(record) && valid_vm_identity?(record)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def self.valid_vm_resources?(record)
|
|
96
|
+
%w[memory cpus].all? { |key| record[key].is_a?(Integer) && record[key].positive? }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def self.valid_vm_identity?(record)
|
|
100
|
+
(record['pid'].nil? || (record['pid'].is_a?(Integer) && record['pid'].positive?)) &&
|
|
101
|
+
%w[preparing running stopped].include?(record['state'])
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def self.valid_vm_network?(name, record)
|
|
105
|
+
record['overlay'] == "vms/#{name}/disk.qcow2" &&
|
|
106
|
+
record['network'].is_a?(String) && record['network'].match?(/\A[a-f0-9]{24}:environment\z/) &&
|
|
107
|
+
record['mac_address'].is_a?(String) &&
|
|
108
|
+
record['mac_address'].match?(/\A52:54:(?:[0-9a-f]{2}:){3}[0-9a-f]{2}\z/)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def self.valid_vm_metadata?(record)
|
|
112
|
+
valid_certificate?(record['certificate_key']) && [true, false].include?(record['provisioned']) &&
|
|
113
|
+
%w[os version created_at].all? { |key| record[key].is_a?(String) && !record[key].empty? }
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def self.valid_definition?(record)
|
|
117
|
+
definition = record['definition']
|
|
118
|
+
definition.is_a?(Hash) && definition['hostname'] == record['hostname'] &&
|
|
119
|
+
definition['image'] == record['image'] && valid_id?(record['id'])
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def self.valid_metadata?(record)
|
|
123
|
+
%w[amd64 arm64].include?(record['architecture']) && valid_certificate?(record['certificate_key']) &&
|
|
124
|
+
%w[os version image created_at].all? do |key|
|
|
125
|
+
record[key].is_a?(String) && !record[key].empty?
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def self.valid_certificate?(key)
|
|
130
|
+
key.nil? || (key.is_a?(String) && key.match?(/\A[a-f0-9]{64}\z/))
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def self.valid_id?(id)
|
|
134
|
+
id.nil? || (id.is_a?(String) && id.match?(/\A[a-zA-Z0-9][a-zA-Z0-9_.-]*\z/))
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def self.valid_name?(name)
|
|
138
|
+
name.is_a?(String) && name.size <= 253 &&
|
|
139
|
+
name.split('.', -1).all? { |label| label.match?(/\A[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\z/) }
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Node
|
|
5
|
+
class PackageBootstrap
|
|
6
|
+
FAMILIES = {
|
|
7
|
+
'ubuntu' => 'debian', 'debian' => 'debian',
|
|
8
|
+
'rhel' => 'redhat', 'rocky' => 'redhat', 'almalinux' => 'redhat', 'oraclelinux' => 'redhat'
|
|
9
|
+
}.freeze
|
|
10
|
+
ACTIONS = %w[install remove].freeze
|
|
11
|
+
GROUPS = %w[default debian redhat].freeze
|
|
12
|
+
|
|
13
|
+
# rubocop:disable-next Metrics/CyclomaticComplexity -- Validate the two-level public package matrix in place.
|
|
14
|
+
def self.validate!(value, path = 'bootstrap.packages', effective: false)
|
|
15
|
+
unless valid_keys?(value, ACTIONS, effective)
|
|
16
|
+
raise ConfigurationError, "#{path} must contain only install and remove mappings"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
value.each do |action, groups|
|
|
20
|
+
unless valid_keys?(groups, GROUPS, effective)
|
|
21
|
+
raise ConfigurationError, "#{path}.#{action} must contain only default, debian and redhat arrays"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
groups.each do |family, packages|
|
|
25
|
+
next if packages.is_a?(Array) && packages.all? { |package| valid_package?(package) }
|
|
26
|
+
|
|
27
|
+
raise ConfigurationError, "#{path}.#{action}.#{family} must contain valid nonempty package names"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.valid_keys?(value, permitted, effective)
|
|
33
|
+
value.is_a?(Hash) && value.keys.all? { |key| permitted.include?(key) } &&
|
|
34
|
+
(!effective || value.keys.sort == permitted.sort)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.valid_package?(package)
|
|
38
|
+
package.is_a?(String) && package == package.strip && !package.empty? &&
|
|
39
|
+
!package.start_with?('-') && !package.match?(/[\x00-\x20\x7f]/)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def initialize(config:, os:, execute:, copy: nil, progress: nil, rpm_options: [])
|
|
43
|
+
@config = config
|
|
44
|
+
@os = os
|
|
45
|
+
@execute = execute
|
|
46
|
+
@copy = copy
|
|
47
|
+
@progress = progress || ->(_message) {}
|
|
48
|
+
@rpm_options = rpm_options
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def required?
|
|
52
|
+
ACTIONS.any? { |action| packages(action).any? }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def run(proxy_url:)
|
|
56
|
+
return unless required?
|
|
57
|
+
|
|
58
|
+
@proxy_url = proxy_url
|
|
59
|
+
@package_proxy = PackageProxy.new(family: family, execute: @execute, copy: @copy)
|
|
60
|
+
@package_proxy.prepare(proxy_url)
|
|
61
|
+
execute('remove', packages('remove'))
|
|
62
|
+
execute('update', []) if packages('install').any?
|
|
63
|
+
execute('install', packages('install'))
|
|
64
|
+
ensure
|
|
65
|
+
@package_proxy&.cleanup
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def family
|
|
71
|
+
FAMILIES.fetch(@os) { raise ConfigurationError, "Unsupported package-bootstrap distribution: #{@os}" }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def packages(action)
|
|
75
|
+
values = @config.fetch(action)
|
|
76
|
+
(values.fetch('default') + values.fetch(family)).uniq
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def execute(action, packages)
|
|
80
|
+
return if action != 'update' && packages.empty?
|
|
81
|
+
|
|
82
|
+
@progress.call(progress_message(action, packages.size))
|
|
83
|
+
arguments = command(action, packages)
|
|
84
|
+
result = @execute.call(arguments)
|
|
85
|
+
return if result.success?
|
|
86
|
+
|
|
87
|
+
raise_failure(action, result)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def raise_failure(action, result)
|
|
91
|
+
manager = family == 'debian' ? 'apt-get' : 'dnf'
|
|
92
|
+
text = PackageProxy.redact("#{result.stderr}\n#{result.stdout}", @proxy_url)
|
|
93
|
+
detail = Execution::Diagnostics.native(result.with(stderr: text, stdout: ''),
|
|
94
|
+
operation: "Package bootstrap #{action}", tool: manager)
|
|
95
|
+
raise Error, "Package bootstrap #{action} failed with #{manager} " \
|
|
96
|
+
"(exit=#{result.exit_status}, timeout=#{result.timed_out}); " \
|
|
97
|
+
"node retained and Puppet was not run\n#{detail}", cause: nil
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def progress_message(action, count)
|
|
101
|
+
return 'Refreshing package metadata...' if action == 'update'
|
|
102
|
+
|
|
103
|
+
verb = action == 'remove' ? 'Removing' : 'Installing'
|
|
104
|
+
"#{verb} #{count} package#{'s' unless count == 1}..."
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def command(action, packages)
|
|
108
|
+
return apt_command(action, packages) if family == 'debian'
|
|
109
|
+
|
|
110
|
+
dnf_command(action, packages)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def apt_command(action, packages)
|
|
114
|
+
if action == 'update'
|
|
115
|
+
return ['apt-get', *@package_proxy.arguments, '-o', 'APT::Update::Error-Mode=any',
|
|
116
|
+
'update']
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
['apt-get', *@package_proxy.arguments, action, '--yes', '--', *packages]
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def dnf_command(action, packages)
|
|
123
|
+
return ['dnf', *@package_proxy.arguments, *@rpm_options, 'makecache'] if action == 'update'
|
|
124
|
+
|
|
125
|
+
['dnf', *@package_proxy.arguments, *@rpm_options, action, '--assumeyes', '--', *packages]
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'tempfile'
|
|
4
|
+
require 'uri'
|
|
5
|
+
|
|
6
|
+
module Empeira
|
|
7
|
+
module Node
|
|
8
|
+
# One private guest configuration for APT/DNF proxy credentials; never put them in argv.
|
|
9
|
+
class PackageProxy
|
|
10
|
+
APT_PATH = '/etc/apt/apt.conf.d/00-empeira-agent-proxy'
|
|
11
|
+
DNF_PATH = '/var/tmp/empeira-agent-dnf.conf'
|
|
12
|
+
|
|
13
|
+
def initialize(family:, execute:, copy:)
|
|
14
|
+
@family = family
|
|
15
|
+
@execute = execute
|
|
16
|
+
@copy = copy
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.copy_text(copy, content, destination, mode: '0600')
|
|
20
|
+
Tempfile.create('empeira-agent-') do |file|
|
|
21
|
+
file.chmod(0o600)
|
|
22
|
+
file.write(content)
|
|
23
|
+
file.flush
|
|
24
|
+
copy.call(file.path, destination, mode)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.redact(diagnostic, proxy_url)
|
|
29
|
+
values = URI(proxy_url).userinfo.to_s.split(':') if proxy_url
|
|
30
|
+
values = [*values, ENV.fetch('EMPEIRA_AGENT_REPO_USERNAME', nil), ENV.fetch('EMPEIRA_AGENT_REPO_PASSWORD', nil)]
|
|
31
|
+
values.compact.reject(&:empty?).sort_by { |value| -value.length }
|
|
32
|
+
.reduce(diagnostic) { |text, value| text.gsub(value, '[REDACTED]') }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def prepare(url)
|
|
36
|
+
content = if @family == 'debian'
|
|
37
|
+
"Acquire::http::Proxy #{url.dump};\nAcquire::https::Proxy #{url.dump};\n"
|
|
38
|
+
else
|
|
39
|
+
dnf_configuration(url)
|
|
40
|
+
end
|
|
41
|
+
self.class.copy_text(@copy, content, path)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def arguments
|
|
45
|
+
@family == 'debian' ? ['-c', path] : ["--config=#{path}"]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def path
|
|
49
|
+
@family == 'debian' ? APT_PATH : DNF_PATH
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def cleanup
|
|
53
|
+
result = @execute.call(['rm', '-f', '--', path])
|
|
54
|
+
return if result.success?
|
|
55
|
+
|
|
56
|
+
diagnostic = Execution::Diagnostics.command(result, operation: 'Temporary package proxy cleanup', tool: 'rm')
|
|
57
|
+
raise Error, "Cannot remove temporary package proxy configuration; Puppet was not run\n#{diagnostic}",
|
|
58
|
+
cause: nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
# rubocop:disable-next Metrics/AbcSize -- Preserve native sections while enforcing private, verified bootstrap settings.
|
|
64
|
+
def dnf_configuration(url)
|
|
65
|
+
lines = original_dnf_lines
|
|
66
|
+
main = lines.index { |line| line.strip == '[main]' }
|
|
67
|
+
raise Error, 'Original DNF configuration has no main section; Puppet was not run' unless main
|
|
68
|
+
|
|
69
|
+
finish = ((main + 1)...lines.size).find { |index| lines[index].lstrip.start_with?('[') } || lines.size
|
|
70
|
+
settings = lines[(main + 1)...finish].grep_v(/\A\s*(?:proxy(?:_[a-z_]+)?|gpgcheck|sslverify|logfilelevel)\s*=/i)
|
|
71
|
+
lines[main] = "[main]\n"
|
|
72
|
+
lines[(main + 1)...finish] = [*dnf_proxy_settings(url), *settings]
|
|
73
|
+
lines.join
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def dnf_proxy_settings(url)
|
|
77
|
+
endpoint = URI(url)
|
|
78
|
+
username = endpoint.user
|
|
79
|
+
password = endpoint.password
|
|
80
|
+
endpoint.user = nil
|
|
81
|
+
endpoint.password = nil
|
|
82
|
+
settings = ["proxy=#{endpoint}\n", "gpgcheck=1\n", "sslverify=1\n", "logfilelevel=0\n"]
|
|
83
|
+
if username || password
|
|
84
|
+
settings.push("proxy_username=#{URI::DEFAULT_PARSER.unescape(username.to_s)}\n",
|
|
85
|
+
"proxy_password=#{URI::DEFAULT_PARSER.unescape(password.to_s)}\n",
|
|
86
|
+
"proxy_auth_method=basic\n")
|
|
87
|
+
end
|
|
88
|
+
settings
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def original_dnf_lines
|
|
92
|
+
result = @execute.call(['cat', '/etc/dnf/dnf.conf'])
|
|
93
|
+
unless result.success?
|
|
94
|
+
diagnostic = Execution::Diagnostics.command(result, operation: 'Original DNF configuration', tool: 'cat')
|
|
95
|
+
raise Error, "Cannot read original DNF configuration; Puppet was not run\n#{diagnostic}", cause: nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
result.stdout.lines
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Node
|
|
5
|
+
module PackageSources
|
|
6
|
+
def self.verify!(execute:)
|
|
7
|
+
script = Pathname(__dir__).join('../../../resources/nodes/apt_sources.rb').read
|
|
8
|
+
result = execute.call([Certificates::RUBY, '-e', script])
|
|
9
|
+
return if result.success?
|
|
10
|
+
|
|
11
|
+
details = Execution::Diagnostics.command(result, operation: 'Active distribution APT source verification',
|
|
12
|
+
tool: 'ruby')
|
|
13
|
+
raise Error, 'Cannot verify an active Ubuntu/Debian distribution APT source before the first catalog. ' \
|
|
14
|
+
'Check /etc/apt/sources.list and sources.list.d/*.list or *.sources in the image/bootstrap; ' \
|
|
15
|
+
"Puppet was not run. Node retained for diagnosis.\n#{details}", cause: nil
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Node
|
|
5
|
+
module PuppetCommand
|
|
6
|
+
def self.arguments
|
|
7
|
+
[Certificates::PUPPET, 'agent', '--test', '--detailed-exitcodes', '--waitforcert', '0',
|
|
8
|
+
'--color', Execution::Terminal.color? ? 'ansi' : 'false']
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pathname'
|
|
4
|
+
|
|
5
|
+
module Empeira
|
|
6
|
+
module Node
|
|
7
|
+
# Restore native DNF repository configuration after the temporary agent source.
|
|
8
|
+
class RpmConfiguration
|
|
9
|
+
DISTRIBUTIONS = %w[rocky almalinux oraclelinux rhel].freeze
|
|
10
|
+
BACKUP = '/root/.empeira-dnf-before-bootstrap.tar'
|
|
11
|
+
|
|
12
|
+
def initialize(os:, execute:, root: '/', backup: BACKUP)
|
|
13
|
+
@os = os
|
|
14
|
+
@execute = execute
|
|
15
|
+
@root = Pathname(root).cleanpath
|
|
16
|
+
@backup = Pathname(backup).cleanpath
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def preserve
|
|
20
|
+
return yield unless DISTRIBUTIONS.include?(@os)
|
|
21
|
+
|
|
22
|
+
capture!
|
|
23
|
+
begin
|
|
24
|
+
yield
|
|
25
|
+
ensure
|
|
26
|
+
restore!
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def capture!
|
|
33
|
+
execute!(['install', '--mode', '0600', '/dev/null', @backup.to_s], 'Cannot prepare DNF configuration backup')
|
|
34
|
+
execute!(['tar', '--create', '--file', @backup.to_s, '--directory', @root.to_s,
|
|
35
|
+
'etc/yum.repos.d', 'etc/dnf'],
|
|
36
|
+
'Cannot preserve base image DNF configuration')
|
|
37
|
+
rescue StandardError
|
|
38
|
+
@execute.call(['rm', '--force', '--', @backup.to_s])
|
|
39
|
+
raise
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def restore!
|
|
43
|
+
message = 'Cannot restore and verify base image DNF configuration; ' \
|
|
44
|
+
'Puppet was not run. Node retained for diagnosis'
|
|
45
|
+
execute!(['tar', '--list', '--file', @backup.to_s], message)
|
|
46
|
+
execute!(['rm', '--recursive', '--force', '--', @root.join('etc/yum.repos.d').to_s,
|
|
47
|
+
@root.join('etc/dnf').to_s], message)
|
|
48
|
+
execute!(['tar', '--extract', '--file', @backup.to_s, '--directory', @root.to_s], message)
|
|
49
|
+
execute!(['tar', '--compare', '--file', @backup.to_s, '--directory', @root.to_s], message)
|
|
50
|
+
execute!(['rm', '--force', '--', @backup.to_s], message)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def execute!(arguments, message)
|
|
54
|
+
result = @execute.call(arguments)
|
|
55
|
+
return if result.success?
|
|
56
|
+
|
|
57
|
+
details = Execution::Diagnostics.command(result, operation: message, tool: arguments.first)
|
|
58
|
+
raise Error, "#{message}\n#{details}", cause: nil
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Node
|
|
5
|
+
RunRequest = Data.define(:hostname, :provider, :os, :version, :memory, :cpus) do
|
|
6
|
+
def self.from_config(hostname:, provider:, config:)
|
|
7
|
+
defaults = config.fetch('node_defaults').slice('os', 'version', 'memory', 'cpus')
|
|
8
|
+
new(hostname: hostname, provider: provider, **defaults.transform_keys(&:to_sym))
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(**attributes)
|
|
12
|
+
validate_hostname!(attributes.fetch(:hostname))
|
|
13
|
+
unless Node.registry.names.include?(attributes.fetch(:provider))
|
|
14
|
+
raise Error, "provider must be one of: #{Node.registry.names.join(', ')}"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
validate_resources!(attributes)
|
|
18
|
+
validate_os!(attributes)
|
|
19
|
+
super(**attributes.transform_values { |value| value.is_a?(String) ? value.dup.freeze : value })
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def validate_resources!(attributes)
|
|
25
|
+
%i[memory cpus].each do |key|
|
|
26
|
+
value = attributes.fetch(key)
|
|
27
|
+
next if value.nil? || (value.is_a?(Integer) && value.positive?)
|
|
28
|
+
|
|
29
|
+
raise Error, "#{key} must be a positive integer or null"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def validate_os!(attributes)
|
|
34
|
+
%i[os version].each do |key|
|
|
35
|
+
value = attributes.fetch(key)
|
|
36
|
+
next if value.nil? || (value.is_a?(String) && !value.strip.empty? && value == value.strip)
|
|
37
|
+
|
|
38
|
+
raise Error, "#{key} must be a non-empty string without surrounding whitespace or null"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def validate_hostname!(hostname)
|
|
43
|
+
label = /\A[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\z/
|
|
44
|
+
return if hostname.is_a?(String) && !hostname.empty? && hostname.length <= 64 &&
|
|
45
|
+
hostname.split('.', -1).all? { |part| part.match?(label) }
|
|
46
|
+
|
|
47
|
+
raise Error, 'hostname must contain DNS labels of 1 to 63 letters, digits or internal hyphens ' \
|
|
48
|
+
'and fit the 64-byte guest kernel hostname limit'
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Node
|
|
5
|
+
# Normal, credential-free guest policy, after verified bootstrap cleanup.
|
|
6
|
+
class RuntimeProxy
|
|
7
|
+
VARIABLES = %w[HTTP_PROXY HTTPS_PROXY http_proxy https_proxy NO_PROXY no_proxy ALL_PROXY all_proxy].freeze
|
|
8
|
+
|
|
9
|
+
def initialize(context:, record:, execute:, persist:)
|
|
10
|
+
@context = context
|
|
11
|
+
@record = record
|
|
12
|
+
@execute = execute
|
|
13
|
+
@persist = persist
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.command(context, arguments)
|
|
17
|
+
environment = ControlPlane::Plan.new(context: context).node_proxy_environment
|
|
18
|
+
['env', *VARIABLES.map { |key| "#{key}=#{environment.fetch(key, '')}" }, *arguments]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.valid_inventory?(entry)
|
|
22
|
+
entry.is_a?(Hash) && (entry.keys - %w[current previous]).empty? && entry.key?('current') &&
|
|
23
|
+
[entry['current'], entry['previous']].compact.all? { |definition| valid_definition?(definition) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.valid_definition?(definition)
|
|
27
|
+
definition.is_a?(Hash) && definition.keys.sort == %w[direct no_proxy url] &&
|
|
28
|
+
definition['url'] == 'http://proxy.empeira.internal:3128' &&
|
|
29
|
+
definition['no_proxy'].is_a?(String) && definition['no_proxy'].match?(/\A[a-zA-Z0-9.,:-]+\z/) &&
|
|
30
|
+
valid_direct?(definition['direct'])
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.valid_direct?(hosts)
|
|
34
|
+
hosts.is_a?(Array) && hosts.all? do |host|
|
|
35
|
+
host.is_a?(String) && host.match?(/\A[a-zA-Z0-9][a-zA-Z0-9.-]*\z/)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def reconcile
|
|
40
|
+
desired = definition
|
|
41
|
+
entry = @record['runtime_proxy']
|
|
42
|
+
return false unless desired || entry
|
|
43
|
+
|
|
44
|
+
recovered = recover(entry) if entry&.key?('previous')
|
|
45
|
+
previous = [entry&.fetch('current')].compact
|
|
46
|
+
@record['runtime_proxy'] = { 'current' => desired, 'previous' => previous.first }
|
|
47
|
+
@persist.call
|
|
48
|
+
changed = execute(desired, previous)
|
|
49
|
+
@record['runtime_proxy'] = { 'current' => desired }
|
|
50
|
+
@persist.call
|
|
51
|
+
changed || recovered || false
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def recover(entry)
|
|
57
|
+
execute(entry.fetch('current'), [entry['current'], entry['previous']].compact)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def definition
|
|
61
|
+
environment = ControlPlane::Plan.new(context: @context).node_proxy_environment
|
|
62
|
+
return if environment.empty?
|
|
63
|
+
|
|
64
|
+
naming = Network::Naming.new
|
|
65
|
+
internal = naming.reserved(@context.configuration).flat_map { |name| naming.aliases(name) }
|
|
66
|
+
{ 'url' => environment.fetch('http_proxy'), 'no_proxy' => environment.fetch('no_proxy'),
|
|
67
|
+
'direct' => ['localhost', '127.0.0.1', *internal,
|
|
68
|
+
*Network::DirectEgress.destinations(@context.configuration.dig('network', 'egress'))].uniq }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def execute(desired, previous)
|
|
72
|
+
helper = Pathname(__dir__).join('../../../resources/nodes/runtime_proxy.rb').read
|
|
73
|
+
request = JSON.generate('desired' => desired, 'accepted' => [desired, *previous].compact)
|
|
74
|
+
result = @execute.call([Certificates::RUBY, '-e', helper, request])
|
|
75
|
+
unless result.success?
|
|
76
|
+
details = Execution::Diagnostics.command(result, operation: 'Normal guest proxy reconciliation', tool: 'ruby')
|
|
77
|
+
raise Error, "Cannot reconcile normal guest proxy; node retained for diagnosis\n#{details}", cause: nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
JSON.parse(result.stdout).fetch('changed')
|
|
81
|
+
rescue JSON::ParserError, KeyError
|
|
82
|
+
raise Error, 'Cannot verify normal guest proxy reconciliation; node retained for diagnosis', cause: nil
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Node
|
|
5
|
+
class Service
|
|
6
|
+
# Preserve explicit injection at the existing provider composition boundary.
|
|
7
|
+
# rubocop:disable-next Metrics/ParameterLists
|
|
8
|
+
def initialize(context:, runner:, providers: Node.registry, runtimes: Runtime.registry, engines: ::Empeira::VM.registry,
|
|
9
|
+
build_info: BuildInfo.load, progress: Progress.new, ssh_preferences: {})
|
|
10
|
+
@context = context
|
|
11
|
+
@runner = runner
|
|
12
|
+
@providers = providers
|
|
13
|
+
@ssh_preferences = Configuration::SSHPreferences.new(ssh_preferences)
|
|
14
|
+
@dependencies = { context: context, runner: runner, runtimes: runtimes, engines: engines,
|
|
15
|
+
build_info: build_info, progress: progress }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def run(request)
|
|
19
|
+
provider(request.provider).run(request)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
%i[start stop inspect_resource shell console exec puppet logs].each do |operation|
|
|
23
|
+
define_method(operation) do |name:, **options|
|
|
24
|
+
record = Infrastructure::Store.new(context: @context).load&.dig('nodes', name.downcase)
|
|
25
|
+
selected = record&.fetch('provider') || 'container'
|
|
26
|
+
provider(selected).public_send(operation, name: name, **options)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def destroy(name:)
|
|
31
|
+
Infrastructure::Cleanup.new(context: @context, runner: @runner,
|
|
32
|
+
runtimes: @dependencies.fetch(:runtimes),
|
|
33
|
+
progress: @dependencies.fetch(:progress)).node(name)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def ssh(name:, user: nil, identity: nil, port: nil)
|
|
37
|
+
record = Infrastructure::Store.new(context: @context).load&.dig('nodes', name.downcase)
|
|
38
|
+
options = @ssh_preferences.resolve(hostname: name, user: user, identity: identity, port: port)
|
|
39
|
+
provider(record&.fetch('provider') || 'container').ssh(name: name, **options)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def list
|
|
43
|
+
provider('container').list + provider('vm').list
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def names
|
|
47
|
+
Infrastructure::Store.new(context: @context).load&.fetch('nodes', {})&.keys || []
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The infrastructure caller already holds the workspace mutation lock.
|
|
51
|
+
# rubocop:disable-next Naming/PredicateMethod -- Reconciliation mutates guest resources.
|
|
52
|
+
def reconcile(state:)
|
|
53
|
+
return false unless state
|
|
54
|
+
|
|
55
|
+
records = state.fetch('nodes', {}).values
|
|
56
|
+
reconciliation_providers(records).map do |name|
|
|
57
|
+
provider(name).reconcile_all(state: state)
|
|
58
|
+
end.any?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def reconciliation_providers(records)
|
|
64
|
+
records.map { |record| record.fetch('provider') }.uniq
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def provider(name)
|
|
68
|
+
@providers.build(name, **@dependencies)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|