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,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Network
|
|
5
|
+
class ProxyPolicy
|
|
6
|
+
LABEL = '[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?'
|
|
7
|
+
DOMAIN = /\A(?:\*\.)?#{LABEL}(?:\.#{LABEL})+\z/
|
|
8
|
+
# Destination IP restrictions apply only to the authenticated bootstrap proxy.
|
|
9
|
+
IPV4_FORBIDDEN = %w[0.0.0.0/8 10.0.0.0/8 100.64.0.0/10 127.0.0.0/8 169.254.0.0/16
|
|
10
|
+
172.16.0.0/12 192.168.0.0/16 224.0.0.0/4 240.0.0.0/4].freeze
|
|
11
|
+
# Squid normalizes mapped addresses to IPv4 before interpreting the prefix.
|
|
12
|
+
# IPv6-sized prefixes are misparsed as ::/0, so retain the equivalent IPv4 prefix.
|
|
13
|
+
MAPPED_FORBIDDEN = IPV4_FORBIDDEN.map { |range| "::ffff:#{range}" }.freeze
|
|
14
|
+
FORBIDDEN = [*IPV4_FORBIDDEN, *MAPPED_FORBIDDEN, '::/128', '::1/128', 'fc00::/7', 'fe80::/10'].join(' ').freeze
|
|
15
|
+
|
|
16
|
+
def self.valid_domain?(value)
|
|
17
|
+
value.is_a?(String) && value.size <= 253 && value.match?(DOMAIN) &&
|
|
18
|
+
!value.delete_prefix('*.').match?(/\A[\d.]+\z/) &&
|
|
19
|
+
!value.end_with?('.empeira.internal', '.localhost') && value != 'empeira.internal'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(config, authorization: nil)
|
|
23
|
+
@config = config
|
|
24
|
+
@authorization = authorization
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def configuration
|
|
28
|
+
[
|
|
29
|
+
'dns_nameservers @EMPEIRA_DNS@', 'http_port 3128',
|
|
30
|
+
'visible_hostname empeira-proxy', 'cache deny all',
|
|
31
|
+
'access_log none', 'cache_log /dev/null', 'pid_filename none',
|
|
32
|
+
client_rule, 'http_access deny !clients',
|
|
33
|
+
'request_header_access Proxy-Authorization deny all',
|
|
34
|
+
'acl SSL_ports port 443', 'acl Safe_ports port 80 443', 'acl CONNECT method CONNECT',
|
|
35
|
+
("acl forbidden dst #{FORBIDDEN}" if @authorization), 'http_access deny !Safe_ports',
|
|
36
|
+
'http_access deny CONNECT !SSL_ports', ('http_access deny forbidden' if @authorization),
|
|
37
|
+
*destination_rules, 'http_access deny all', ''
|
|
38
|
+
].compact.join("\n")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.domain_rules(domains, name: 'permitted', condition: nil)
|
|
42
|
+
return [] if domains.empty?
|
|
43
|
+
|
|
44
|
+
destinations = domains.map { |domain| domain.start_with?('*.') ? ".#{domain.delete_prefix('*.')}" : domain }
|
|
45
|
+
["acl #{name} dstdomain -n #{destinations.join(' ')}",
|
|
46
|
+
"http_access allow #{[condition, name].compact.join(' ')}"]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def destination_rules
|
|
52
|
+
return self.class.domain_rules(@config.fetch('global')) if @authorization
|
|
53
|
+
|
|
54
|
+
['include /empeira-proxy/proxy-rules.conf']
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def client_rule
|
|
58
|
+
return 'acl clients src "/empeira-proxy/proxy-clients"' unless @authorization
|
|
59
|
+
|
|
60
|
+
"acl clients req_header Proxy-Authorization ^Basic[[:space:]]#{Regexp.escape(@authorization)}$"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Network
|
|
5
|
+
# Observed owned service addresses are transient gateway inputs, never inventory mappings.
|
|
6
|
+
class Redirects
|
|
7
|
+
EXCLUDED_TARGETS = %w[gateway browser-ui].freeze
|
|
8
|
+
|
|
9
|
+
def self.target_key(entry)
|
|
10
|
+
key = entry.fetch('to').fetch('service')
|
|
11
|
+
key == 'coredns' ? 'dns' : key
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.validate_routes!(entries, routes:)
|
|
15
|
+
entries.each_with_index do |entry, index|
|
|
16
|
+
source = entry.fetch('from').fetch('ip')
|
|
17
|
+
next unless DirectEgress::BENCHMARK.include?(source)
|
|
18
|
+
|
|
19
|
+
collision = routes.find { |range| IPAddr.new(range).include?(source) }
|
|
20
|
+
next unless collision
|
|
21
|
+
|
|
22
|
+
raise ConfigurationError,
|
|
23
|
+
"network.redirects[#{index}].from.ip collides with host/VPN/runtime route #{collision}"
|
|
24
|
+
end
|
|
25
|
+
rescue IPAddr::InvalidAddressError
|
|
26
|
+
raise Error, 'Cannot verify virtual redirect source route collisions', cause: nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.validate!(entries, definitions:, subnet: nil)
|
|
30
|
+
entries.each_with_index do |entry, index|
|
|
31
|
+
path = "network.redirects[#{index}]"
|
|
32
|
+
target = target_key(entry)
|
|
33
|
+
unless definitions.key?(target) && !EXCLUDED_TARGETS.include?(target)
|
|
34
|
+
raise ConfigurationError, "#{path}.to.service #{target} is missing, disabled or not an internal " \
|
|
35
|
+
'application service'
|
|
36
|
+
end
|
|
37
|
+
next unless subnet && IPAddr.new(subnet).include?(entry.fetch('from').fetch('ip'))
|
|
38
|
+
|
|
39
|
+
raise ConfigurationError, "#{path}.from.ip collides with the workspace subnet #{subnet}"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def initialize(entries:, network:, subnet:)
|
|
44
|
+
@entries = entries
|
|
45
|
+
@network = network
|
|
46
|
+
@subnet = IPAddr.new(subnet)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def resolve(definitions:, resources:)
|
|
50
|
+
entries = @entries.map { |entry| resolved_entry(entry, definitions, resources) }
|
|
51
|
+
entries.sort_by do |entry|
|
|
52
|
+
entry.fetch('from').values_at('ip', 'port') + [entry.fetch('from').fetch('protocol', 'tcp')]
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def resolved_entry(entry, definitions, resources)
|
|
59
|
+
key = self.class.target_key(entry)
|
|
60
|
+
ip = target_address(resources[key], definitions.fetch(key))
|
|
61
|
+
source = entry.fetch('from').dup
|
|
62
|
+
source.delete('protocol') if source['protocol'] == 'tcp'
|
|
63
|
+
{ 'from' => source, 'to' => ip && { 'ip' => ip, 'port' => entry.fetch('to').fetch('port') } }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def target_address(resource, definition)
|
|
67
|
+
return unless resource
|
|
68
|
+
|
|
69
|
+
definition.verify!(resource)
|
|
70
|
+
return unless resource['state'] == 'running' &&
|
|
71
|
+
resource.dig('labels', 'io.empeira.definition') == definition.fingerprint
|
|
72
|
+
|
|
73
|
+
ip = resource.dig('networks', @network, 'IPAddress')
|
|
74
|
+
unless valid_address?(ip)
|
|
75
|
+
raise Providers::OwnershipError, "Redirect target #{definition.key} has no valid workspace IPv4 address"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
ip
|
|
79
|
+
rescue IPAddr::InvalidAddressError
|
|
80
|
+
raise Providers::OwnershipError, "Redirect target #{definition.key} has an invalid workspace address",
|
|
81
|
+
cause: nil
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def valid_address?(ip)
|
|
85
|
+
ip.is_a?(String) && @subnet.include?(IPAddr.new(ip)) && IPAddr.new(ip).to_s == ip
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Network
|
|
5
|
+
module ResourceIdentity
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def valid_identity?(value)
|
|
9
|
+
value.is_a?(String) && value.match?(/\A[a-zA-Z0-9][a-zA-Z0-9_.-]*\z/)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def valid_labels?(labels)
|
|
13
|
+
labels.is_a?(Hash) && labels.all? { |key, value| key.is_a?(String) && value.is_a?(String) }
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Identity and isolation only; this cannot stand in for a deletion membership observation.
|
|
18
|
+
Binding = Data.define(:id, :name, :labels, :isolated) do
|
|
19
|
+
include ResourceIdentity
|
|
20
|
+
|
|
21
|
+
def initialize(id:, name:, labels:, isolated:)
|
|
22
|
+
unless valid_identity?(id) && valid_identity?(name) && valid_labels?(labels) && [true, false].include?(isolated)
|
|
23
|
+
raise Providers::ExecutionError, 'Runtime returned malformed network binding'
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
super(id: id.dup.freeze, name: name.dup.freeze, labels: Immutable.deep_freeze(labels.dup), isolated: isolated)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
Resource = Data.define(:id, :name, :labels, :isolated, :attachment_count) do
|
|
31
|
+
include ResourceIdentity
|
|
32
|
+
|
|
33
|
+
def initialize(id:, name:, labels:, isolated:, attachment_count:)
|
|
34
|
+
unless valid_identity?(id) && valid_identity?(name) && valid_labels?(labels) &&
|
|
35
|
+
valid_policy?(isolated, attachment_count)
|
|
36
|
+
raise Providers::ExecutionError, 'Runtime returned malformed network data'
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
super(id: id.dup.freeze, name: name.dup.freeze, labels: Immutable.deep_freeze(labels.dup),
|
|
40
|
+
isolated: isolated, attachment_count: attachment_count)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def valid_policy?(isolated, count)
|
|
46
|
+
[true, false].include?(isolated) && count.is_a?(Integer) && count >= 0
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Node
|
|
5
|
+
module AgentInstaller
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def build(context:, runtime:, requirements:, execute:, copy:, progress: Progress.new)
|
|
9
|
+
AgentInstallation.new(context: context, runtime: runtime, requirements: requirements,
|
|
10
|
+
execute: execute, copy: copy, progress: progress)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class AgentInstallation
|
|
15
|
+
def initialize(context:, runtime:, requirements:, execute:, copy:, progress:)
|
|
16
|
+
@acquisition = Agent::Acquisition.new(context: context, runtime: runtime, progress: progress)
|
|
17
|
+
@requirements = requirements
|
|
18
|
+
@execute = execute
|
|
19
|
+
@copy = copy
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def install(proxy_url:)
|
|
23
|
+
@acquisition.with_package(@requirements) do |artifact|
|
|
24
|
+
AgentPackage.new(target: @requirements.target, package: @requirements.agent.fetch('package'),
|
|
25
|
+
artifact: artifact, execute: @execute, copy: @copy,
|
|
26
|
+
rpm_options: @requirements.rpm_options).install(proxy_url: proxy_url)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
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
|
+
# Installs one host-acquired artifact through the guest's native manager and original base sources.
|
|
8
|
+
class AgentPackage
|
|
9
|
+
PACKAGE_PATH = '/var/tmp/empeira-agent-package'
|
|
10
|
+
|
|
11
|
+
def initialize(target:, package:, artifact:, execute:, copy:, rpm_options: [])
|
|
12
|
+
@target = target
|
|
13
|
+
@package = package
|
|
14
|
+
@artifact = artifact
|
|
15
|
+
@execute = execute
|
|
16
|
+
@copy = copy
|
|
17
|
+
@rpm_options = rpm_options
|
|
18
|
+
@paths = []
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def install(proxy_url:)
|
|
22
|
+
@proxy_url = proxy_url
|
|
23
|
+
@package_proxy = PackageProxy.new(family: @target.family, execute: @execute, copy: @copy)
|
|
24
|
+
@paths << @package_proxy.path
|
|
25
|
+
@package_proxy.prepare(proxy_url)
|
|
26
|
+
@paths << package_path
|
|
27
|
+
@copy.call(@artifact.path, package_path, '0600')
|
|
28
|
+
install_keys if @target.format == 'rpm'
|
|
29
|
+
install_package
|
|
30
|
+
verify_version!
|
|
31
|
+
ensure
|
|
32
|
+
cleanup
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def cleanup
|
|
38
|
+
remove_introduced_keys if @original_keys
|
|
39
|
+
ensure
|
|
40
|
+
execute!(['rm', '-f', '--', *@paths], 'local agent artifact cleanup') unless @paths.empty?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def package_path
|
|
44
|
+
"#{PACKAGE_PATH}.#{@target.format}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def install_package
|
|
48
|
+
if @target.format == 'deb'
|
|
49
|
+
options = ['-o', 'Acquire::http::AllowRedirect=false', '-o', 'Acquire::https::AllowRedirect=false',
|
|
50
|
+
'-o', 'APT::Update::Error-Mode=any']
|
|
51
|
+
execute!(['apt-get', *@package_proxy.arguments, *options, 'update'], 'base dependency metadata acquisition')
|
|
52
|
+
execute!(['apt-get', *@package_proxy.arguments, *options, 'install', '-y', '--', package_path],
|
|
53
|
+
'local APT agent installation (dependencies must be available in the original base sources)')
|
|
54
|
+
else
|
|
55
|
+
verification = @artifact.metadata.fetch('verify_signatures') ? 1 : 0
|
|
56
|
+
execute!(['dnf', *@package_proxy.arguments, "--setopt=localpkg_gpgcheck=#{verification}", *@rpm_options,
|
|
57
|
+
'install', '--assumeyes', '--', package_path],
|
|
58
|
+
'local DNF agent installation (dependencies must be available in the original base sources)')
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def install_keys
|
|
63
|
+
@original_keys = signing_keys
|
|
64
|
+
@artifact.metadata.fetch('public_keys').each_with_index do |encoded, index|
|
|
65
|
+
path = "/var/tmp/empeira-agent-signing-key-#{index}"
|
|
66
|
+
@paths << path
|
|
67
|
+
PackageProxy.copy_text(@copy, Base64.strict_decode64(encoded), path)
|
|
68
|
+
execute!(['rpmkeys', '--import', path], 'temporary agent signing key import')
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def signing_keys
|
|
73
|
+
result = execute!(['rpm', '-qa', '--qf', "%{NAME}-%{VERSION}-%{RELEASE}\n", 'gpg-pubkey'],
|
|
74
|
+
'agent signing key ownership verification')
|
|
75
|
+
result.stdout.lines.map(&:strip).sort.tap do |keys|
|
|
76
|
+
raise Error, 'Cannot validate RPM signing key identity; Puppet was not run' unless
|
|
77
|
+
keys.all? { |key| key.match?(/\Agpg-pubkey-[a-f0-9]+-[a-f0-9]+\z/) }
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def remove_introduced_keys
|
|
82
|
+
(signing_keys - @original_keys).each do |key|
|
|
83
|
+
execute!(['rpm', '-e', '--', key], 'temporary signing key cleanup')
|
|
84
|
+
end
|
|
85
|
+
raise Error, 'Cannot verify agent signing key cleanup; Puppet was not run' unless signing_keys == @original_keys
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def installed_identity
|
|
89
|
+
command = identity_command
|
|
90
|
+
result = execute!(command, 'installed agent identity verification')
|
|
91
|
+
name, version, architecture = result.stdout.strip.split('|')
|
|
92
|
+
version = version.delete_prefix('0:') if @target.format == 'rpm' && version
|
|
93
|
+
[name, version, architecture]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def identity_command
|
|
97
|
+
if @target.format == 'deb'
|
|
98
|
+
['dpkg-query', '-W', '-f=${Package}|${Version}|${Architecture}', @package]
|
|
99
|
+
else
|
|
100
|
+
['rpm', '-q', '--qf', '%{NAME}|%{EPOCHNUM}:%{VERSION}-%{RELEASE}|%{ARCH}', @package]
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def verify_version!
|
|
105
|
+
name, version, architecture = installed_identity
|
|
106
|
+
return if name == @package && version == @artifact.metadata.fetch('version') &&
|
|
107
|
+
architecture == @artifact.metadata.fetch('architecture')
|
|
108
|
+
|
|
109
|
+
raise Error, 'Installed agent package identity differs from the resolved artifact; Puppet was not run'
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def execute!(arguments, operation)
|
|
113
|
+
result = @execute.call(arguments)
|
|
114
|
+
return result if result.success?
|
|
115
|
+
|
|
116
|
+
text = PackageProxy.redact("#{result.stderr}\n#{result.stdout}", @proxy_url)
|
|
117
|
+
diagnostic = Execution::Diagnostics.native(result.with(stderr: text, stdout: ''),
|
|
118
|
+
operation: "Agent #{operation}", tool: arguments.first)
|
|
119
|
+
raise Error, "Agent #{operation} failed; node retained and Puppet was not run\n#{diagnostic}", cause: nil
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# rubocop:enable Style/FormatStringToken
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'uri'
|
|
4
|
+
require 'digest'
|
|
5
|
+
require 'base64'
|
|
6
|
+
|
|
7
|
+
module Empeira
|
|
8
|
+
module Node
|
|
9
|
+
# Native source setup and package inspection run only in an owned disposable helper.
|
|
10
|
+
# rubocop:disable-next Metrics/ClassLength -- APT metadata resolution and package inspection share one isolated source.
|
|
11
|
+
class AgentRepository
|
|
12
|
+
include Agent::NativeCommands
|
|
13
|
+
|
|
14
|
+
AUTH_PATH = '/etc/apt/auth.conf.d/00-empeira-agent.conf'
|
|
15
|
+
SOURCE_PATH = '/var/tmp/empeira-agent-sources/agent.list'
|
|
16
|
+
KEY_PATH = '/var/tmp/empeira-agent-key.gpg'
|
|
17
|
+
PACKAGE_PATH = '/var/tmp/empeira-agent-package.deb'
|
|
18
|
+
attr_reader :public_keys
|
|
19
|
+
|
|
20
|
+
# rubocop:disable-next Metrics/ParameterLists -- The helper receives explicit transport and target dependencies.
|
|
21
|
+
def initialize(source:, package:, version:, target:, execute:, copy:, download:, authentication:, directory:)
|
|
22
|
+
@source = source
|
|
23
|
+
@package = package
|
|
24
|
+
@version = version
|
|
25
|
+
@target = target
|
|
26
|
+
@execute = execute
|
|
27
|
+
@copy = copy
|
|
28
|
+
@download = download
|
|
29
|
+
@authentication = authentication
|
|
30
|
+
@directory = directory
|
|
31
|
+
@public_keys = []
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def resolve
|
|
35
|
+
@release_package = nil
|
|
36
|
+
install_source
|
|
37
|
+
native!(['apt-get', *apt_options, 'update'], 'APT metadata acquisition')
|
|
38
|
+
result = native!(['apt-cache', *apt_options, 'show', '--', @package], 'APT package resolution')
|
|
39
|
+
candidates = result.stdout.split(/\n\s*\n/).filter_map { |stanza| apt_candidate(stanza) }
|
|
40
|
+
Agent::Versions.select(candidates, @version, suffix: @source['suffix'])
|
|
41
|
+
ensure
|
|
42
|
+
remove_release_package if @release_package
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def inspect_package(path)
|
|
46
|
+
@copy.call(path, PACKAGE_PATH, '0600')
|
|
47
|
+
result = execute!(['dpkg-deb', '--show', '--showformat=${Package}|${Version}|${Architecture}', PACKAGE_PATH],
|
|
48
|
+
'DEB package inspection')
|
|
49
|
+
name, version, architecture = result.stdout.strip.split('|')
|
|
50
|
+
validate_metadata(name, version, architecture)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def prepare_keys
|
|
54
|
+
return unless @source['key']
|
|
55
|
+
|
|
56
|
+
path = download_artifact(@source.fetch('key'), 'signing-key')
|
|
57
|
+
@public_keys << Base64.strict_encode64(File.binread(path))
|
|
58
|
+
key = File.binread(path).start_with?('-----BEGIN PGP PUBLIC KEY BLOCK-----') ? "#{KEY_PATH}.asc" : KEY_PATH
|
|
59
|
+
@copy.call(path, key, '0644')
|
|
60
|
+
key
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def install_source
|
|
66
|
+
execute!(['mkdir', '-p', File.dirname(SOURCE_PATH), '/var/tmp/empeira-agent-lists/partial'],
|
|
67
|
+
'source preparation')
|
|
68
|
+
if @source['release'] || !@source.key?('suite')
|
|
69
|
+
install_release(@source['release'] || @source)
|
|
70
|
+
collect_release_sources
|
|
71
|
+
else
|
|
72
|
+
install_repository
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def install_repository
|
|
77
|
+
key = prepare_keys
|
|
78
|
+
options = []
|
|
79
|
+
options << "signed-by=#{key}" if key
|
|
80
|
+
options << 'trusted=yes' if @source['verify_signatures'] == false
|
|
81
|
+
line = "deb [#{options.join(' ')}] #{@source.fetch('url')} #{@source.fetch('suite')} " \
|
|
82
|
+
"#{@source.fetch('component')}\n"
|
|
83
|
+
copy_text(line.sub('[] ', ''), SOURCE_PATH, mode: '0644')
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def install_release(artifact)
|
|
87
|
+
path = download_artifact(artifact, 'release.deb')
|
|
88
|
+
destination = '/var/tmp/empeira-agent-release.deb'
|
|
89
|
+
@copy.call(path, destination, '0600')
|
|
90
|
+
name = execute!(['dpkg-deb', '--field', destination, 'Package'], 'release metadata').stdout.strip
|
|
91
|
+
require_absent_release!(name)
|
|
92
|
+
@release_package = name
|
|
93
|
+
execute!(['dpkg', '-i', destination], 'release package installation')
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def collect_release_sources
|
|
97
|
+
files = execute!(['dpkg-query', '-L', @release_package], 'release source discovery').stdout.lines.map(&:strip)
|
|
98
|
+
files = files.select do |file|
|
|
99
|
+
file.start_with?('/etc/apt/sources.list.d/') && file.match?(/\.(?:list|sources)\z/)
|
|
100
|
+
end
|
|
101
|
+
raise Error, 'Agent release package provides no native APT source' if files.empty?
|
|
102
|
+
|
|
103
|
+
files.each_with_index do |file, index|
|
|
104
|
+
copy_release_source(file, index)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def copy_release_source(file, index)
|
|
109
|
+
content = execute!(['cat', file], 'release source configuration').stdout
|
|
110
|
+
content = Agent::ReleaseSources.apt(content, format: File.extname(file), source: @source)
|
|
111
|
+
copy_text(content, "/var/tmp/empeira-agent-sources/#{index}#{File.extname(file)}", mode: '0644')
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def apt_options
|
|
115
|
+
['-o', 'Dir::Etc::sourcelist=/dev/null', '-o', 'Dir::Etc::sourceparts=/var/tmp/empeira-agent-sources',
|
|
116
|
+
'-o', 'Dir::State::lists=/var/tmp/empeira-agent-lists', '-o', 'Dir::State::status=/dev/null',
|
|
117
|
+
'-o', 'Acquire::http::AllowRedirect=false', '-o', 'Acquire::https::AllowRedirect=false',
|
|
118
|
+
'-o', 'APT::Update::Error-Mode=any']
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def apt_candidate(stanza)
|
|
122
|
+
fields = apt_fields(stanza)
|
|
123
|
+
return unless apt_identity?(fields) && %w[Filename Version SHA256].all? { |key| fields[key] }
|
|
124
|
+
|
|
125
|
+
return unless Agent::Versions.match?(fields.fetch('Version'), @version, suffix: @source['suffix'])
|
|
126
|
+
|
|
127
|
+
{ 'version' => fields.fetch('Version'), 'architecture' => fields.fetch('Architecture'),
|
|
128
|
+
'url' => package_url(fields.fetch('Version')), 'sha256' => fields.fetch('SHA256') }
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def apt_fields(stanza)
|
|
132
|
+
stanza.lines.filter_map do |line|
|
|
133
|
+
line.split(': ', 2) if line.match?(/\A\w[^:]*: /)
|
|
134
|
+
end.to_h.transform_values(&:strip)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def apt_identity?(fields)
|
|
138
|
+
fields['Package'] == @package && [@target.native_architecture, 'all'].include?(fields['Architecture'])
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def package_url(version)
|
|
142
|
+
result = native!(['apt-get', *apt_options, '--print-uris', 'download', "#{@package}=#{version}"],
|
|
143
|
+
'APT artifact location')
|
|
144
|
+
urls = result.stdout.lines.filter_map { |line| line[%r{\A'(https://[^']+)'}, 1] }.uniq
|
|
145
|
+
raise Error, 'Selected agent source did not provide one HTTPS package location' unless urls.size == 1
|
|
146
|
+
|
|
147
|
+
urls.first
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def validate_metadata(name, version, architecture)
|
|
151
|
+
unless name == @package && [@target.native_architecture, 'all'].include?(architecture) &&
|
|
152
|
+
Agent::Versions.match?(version.to_s, @version, suffix: @source['suffix'])
|
|
153
|
+
raise Error, 'Agent package name, native version or architecture differs from the requested target'
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
{ 'version' => version, 'architecture' => architecture }
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Node
|
|
5
|
+
# Preserve the base image's complete APT configuration around temporary
|
|
6
|
+
# agent-repository and package-bootstrap changes.
|
|
7
|
+
class AptConfiguration
|
|
8
|
+
DISTRIBUTIONS = %w[ubuntu debian].freeze
|
|
9
|
+
BACKUP = '/root/.empeira-apt-before-bootstrap.tar'
|
|
10
|
+
|
|
11
|
+
def initialize(os:, execute:, root: '/', backup: BACKUP, on_retry: nil)
|
|
12
|
+
@os = os
|
|
13
|
+
@execute = execute
|
|
14
|
+
@root = Pathname(root).cleanpath
|
|
15
|
+
@backup = Pathname(backup).cleanpath
|
|
16
|
+
@on_retry = on_retry
|
|
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 APT configuration backup')
|
|
34
|
+
execute!(['tar', '--create', '--file', @backup.to_s, '--directory', @root.to_s, 'etc/apt'],
|
|
35
|
+
'Cannot preserve the base image APT configuration')
|
|
36
|
+
rescue StandardError => e
|
|
37
|
+
cleanup = @execute.call(['rm', '--force', '--', @backup.to_s])
|
|
38
|
+
unless cleanup.success?
|
|
39
|
+
details = Execution::Diagnostics.command(cleanup, operation: 'Partial APT backup cleanup', tool: 'rm')
|
|
40
|
+
raise Error, "#{e.message}\nPartial APT backup cleanup also failed\n#{details}", cause: nil
|
|
41
|
+
end
|
|
42
|
+
raise
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def restore!
|
|
46
|
+
execute!(['tar', '--list', '--file', @backup.to_s], restore_error)
|
|
47
|
+
execute!(['rm', '--recursive', '--force', '--', @root.join('etc/apt').to_s], restore_error)
|
|
48
|
+
execute!(['tar', '--extract', '--file', @backup.to_s, '--directory', @root.to_s], restore_error)
|
|
49
|
+
execute!(['tar', '--compare', '--file', @backup.to_s, '--directory', @root.to_s], restore_error)
|
|
50
|
+
remove_backup!
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def remove_backup!
|
|
54
|
+
retried = false
|
|
55
|
+
begin
|
|
56
|
+
execute!(['rm', '--force', '--', @backup.to_s],
|
|
57
|
+
'Cannot remove the restored APT configuration backup; ' \
|
|
58
|
+
'Puppet was not run. Node retained for diagnosis')
|
|
59
|
+
rescue ::Empeira::VM::Guest::TransportError => e
|
|
60
|
+
raise unless e.transient? && !retried
|
|
61
|
+
|
|
62
|
+
retried = true
|
|
63
|
+
@on_retry&.call("Retrying only the restored APT backup removal once after a transient SSH error.\n" \
|
|
64
|
+
"#{e.message}")
|
|
65
|
+
retry
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def restore_error
|
|
70
|
+
'Cannot restore and verify the base image APT configuration; Puppet was not run. Node retained for diagnosis'
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def execute!(arguments, message)
|
|
74
|
+
result = @execute.call(arguments)
|
|
75
|
+
return if result.success?
|
|
76
|
+
|
|
77
|
+
details = Execution::Diagnostics.command(result, operation: "#{message} (#{arguments.first} #{arguments[1]})",
|
|
78
|
+
tool: arguments.first)
|
|
79
|
+
raise Error, "#{message}\n#{details}", cause: nil
|
|
80
|
+
rescue ::Empeira::VM::Guest::TransportError => e
|
|
81
|
+
raise e.with_context(message), cause: nil
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Node
|
|
5
|
+
# The complete Empeira-owned guest file state required before the first catalog.
|
|
6
|
+
class Bootstrap
|
|
7
|
+
FileEntry = Struct.new(:path, :content, :mode, keyword_init: true)
|
|
8
|
+
|
|
9
|
+
attr_reader :files
|
|
10
|
+
|
|
11
|
+
def initialize(provider:)
|
|
12
|
+
@files = [FileEntry.new(path: ExternalFact::PATH, content: ExternalFact.content(provider),
|
|
13
|
+
mode: 0o644).freeze].freeze
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|