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,328 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Infrastructure
|
|
5
|
+
# One recovery transaction over the existing inventory and native lifecycle adapters.
|
|
6
|
+
# rubocop:disable-next Metrics/ClassLength -- One recovery transaction retains ownership until native removals are confirmed.
|
|
7
|
+
class Cleanup
|
|
8
|
+
def initialize(context:, runner:, runtimes: Runtime.registry, runtime: nil, progress: Progress.new)
|
|
9
|
+
@context = context
|
|
10
|
+
@runner = runner
|
|
11
|
+
@runtimes = runtimes
|
|
12
|
+
@runtime = runtime
|
|
13
|
+
@progress = progress
|
|
14
|
+
@store = Store.new(context: context, recovery: true)
|
|
15
|
+
@errors = []
|
|
16
|
+
@failures = []
|
|
17
|
+
@changed = false
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def destroy
|
|
21
|
+
transaction do
|
|
22
|
+
@state.fetch('nodes', {}).dup.each_key { |name| attempt("node #{name}") { remove_node(name) } }
|
|
23
|
+
remove_bootstrap
|
|
24
|
+
remove_services
|
|
25
|
+
remove_volumes
|
|
26
|
+
remove_networks
|
|
27
|
+
reject_unknown_resources
|
|
28
|
+
if @errors.empty?
|
|
29
|
+
cleanup_files
|
|
30
|
+
@store.clear
|
|
31
|
+
end
|
|
32
|
+
report_errors!
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def node(name)
|
|
37
|
+
hostname = name.downcase
|
|
38
|
+
raise ConfigurationError, 'Invalid node hostname' unless Node::Inventory.valid_name?(hostname)
|
|
39
|
+
|
|
40
|
+
transaction do
|
|
41
|
+
attempt("node #{hostname}") { remove_node(hostname) }
|
|
42
|
+
attempt('node discovery records') { remove_dns(hostname) } unless @state.fetch('nodes', {}).key?(hostname)
|
|
43
|
+
report_errors!
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def transaction
|
|
50
|
+
@errors.clear
|
|
51
|
+
@failures.clear
|
|
52
|
+
@changed = false
|
|
53
|
+
@store.with_lock do
|
|
54
|
+
@state = @store.load
|
|
55
|
+
next Providers::Result.new(resource: nil, changed: false) unless @state
|
|
56
|
+
|
|
57
|
+
@runtime ||= @runtimes.build(@state.fetch('runtime'), context: @context, runner: @runner)
|
|
58
|
+
@runtime.check_available!
|
|
59
|
+
yield
|
|
60
|
+
Providers::Result.new(resource: nil, changed: @changed)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def attempt(identity)
|
|
65
|
+
yield
|
|
66
|
+
rescue Error, SystemCallError, KeyError, ArgumentError => e
|
|
67
|
+
@failures << e
|
|
68
|
+
diagnostic = Execution::Diagnostics.safe_text(e.message)
|
|
69
|
+
@errors << "#{identity}: #{diagnostic}"
|
|
70
|
+
rescue TypeError, NoMethodError => e
|
|
71
|
+
@failures << e
|
|
72
|
+
@errors << "#{identity}: Resource fields cannot be safely interpreted (#{e.class.name}); retained"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def save
|
|
76
|
+
@store.write(@state)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def report_errors!
|
|
80
|
+
return if @errors.empty?
|
|
81
|
+
|
|
82
|
+
failure = @failures.any? && @failures.all?(Locked) ? Locked : Error
|
|
83
|
+
raise failure, "Cleanup is incomplete; inventory and remaining ownership data retained.\n#{@errors.join("\n")}",
|
|
84
|
+
cause: nil
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def remove_node(name)
|
|
88
|
+
record = @state.fetch('nodes', {})[name]
|
|
89
|
+
return unless record
|
|
90
|
+
|
|
91
|
+
RecoveryInventory.node!(name, record)
|
|
92
|
+
if record['provider'] == 'vm'
|
|
93
|
+
VM::Removal.new(context: @context, runner: @runner, runtime: @runtime, store: @store).remove(record, @state)
|
|
94
|
+
else
|
|
95
|
+
remove_container(record)
|
|
96
|
+
end
|
|
97
|
+
retire_certificate(record)
|
|
98
|
+
remove_session_lock(record) if record['provider'] == 'vm'
|
|
99
|
+
@state.fetch('nodes').delete(name)
|
|
100
|
+
save
|
|
101
|
+
@changed = true
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def remove_session_lock(record)
|
|
105
|
+
path = @store.directory.join("vm-session-#{record.fetch('peer').fetch('token')}.lock")
|
|
106
|
+
OwnedFiles.remove(path, root: @store.directory)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def remove_container(record)
|
|
110
|
+
name = record.fetch('hostname')
|
|
111
|
+
path = @store.directory.join('containers', name)
|
|
112
|
+
OwnedFiles.verify_removable!(path, root: @store.directory)
|
|
113
|
+
definition = Node::Definition.new(hostname: name, workspace: @context.workspace)
|
|
114
|
+
resource = @runtime.inspect_service(definition, expected_id: record['id'])
|
|
115
|
+
if resource
|
|
116
|
+
verify_container_network!(resource, record)
|
|
117
|
+
recover_container_id!(resource, record) unless record['id']
|
|
118
|
+
remove_service(definition, record['id'])
|
|
119
|
+
end
|
|
120
|
+
OwnedFiles.remove(path, root: @store.directory)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def verify_container_network!(resource, record)
|
|
124
|
+
network = Network::Definition.new(workspace: @context.workspace, policy: Network::Policy.new).backend_name
|
|
125
|
+
return if resource.fetch('networks').keys == [network] && Node::SSHEndpoint.valid?(resource, record)
|
|
126
|
+
|
|
127
|
+
raise Providers::OwnershipError, 'Node has unexpected network membership or published ports; retained'
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# rubocop:disable-next Metrics/AbcSize -- Match persisted random instance intent before recording a recovered ID.
|
|
131
|
+
def recover_container_id!(resource, record)
|
|
132
|
+
options = record.fetch('definition')
|
|
133
|
+
definition = Node::Definition.new(hostname: record.fetch('hostname'), workspace: @context.workspace,
|
|
134
|
+
**options.except('hostname').transform_keys(&:to_sym))
|
|
135
|
+
network = resource.fetch('networks').values.first
|
|
136
|
+
mac = options['mac_address']
|
|
137
|
+
unless mac.is_a?(String) && mac.match?(/\A02:(?:[0-9a-f]{2}:){4}[0-9a-f]{2}\z/) &&
|
|
138
|
+
network['MacAddress'] == mac && resource.dig('labels', 'io.empeira.definition') == definition.fingerprint
|
|
139
|
+
raise Providers::OwnershipError, 'Uncertain node creation has no matching instance intent; retained'
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
record['id'] = resource.fetch('id')
|
|
143
|
+
save
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def retire_certificate(record)
|
|
147
|
+
key = record['certificate_key']
|
|
148
|
+
return unless key
|
|
149
|
+
unless Node::Inventory.valid_certificate?(key)
|
|
150
|
+
raise StateError, 'Recorded certificate identity is invalid; ownership data retained'
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
@state['retired_certificates'] ||= {}
|
|
154
|
+
@state.fetch('retired_certificates')[record.fetch('hostname')] = key
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def remove_dns(name)
|
|
158
|
+
files = ControlPlane::Files.new(context: @context)
|
|
159
|
+
path = Pathname(files.path('hosts'))
|
|
160
|
+
return unless OwnedFiles.verify_removable!(path, root: @store.directory)
|
|
161
|
+
|
|
162
|
+
aliases = Network::Naming.new.aliases(name)
|
|
163
|
+
content = path.readlines.reject { |line| line.split.drop(1).intersect?(aliases) }.join
|
|
164
|
+
@changed = files.write('hosts', content) || @changed
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def remove_service(definition, id)
|
|
168
|
+
raise StateError, 'Invalid recorded service ID' unless Node::Inventory.valid_id?(id)
|
|
169
|
+
|
|
170
|
+
resource = @runtime.inspect_service(definition, expected_id: id)
|
|
171
|
+
return unless resource
|
|
172
|
+
raise Providers::OwnershipError, 'Service has no recorded instance ID; resource retained' unless id
|
|
173
|
+
|
|
174
|
+
@runtime.remove_service(definition, expected_id: id)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def identity(key)
|
|
178
|
+
unless key.is_a?(String) && key.match?(/\A[a-z][a-z0-9]*(?:-[a-z0-9]+)*\z/)
|
|
179
|
+
raise StateError, 'Invalid service identity; resource retained'
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
Services::Definition.new(key: key, workspace: @context.workspace)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def plane
|
|
186
|
+
@state.fetch('control_plane', {})
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def remove_bootstrap
|
|
190
|
+
return unless @state.key?('bootstrap_proxy')
|
|
191
|
+
|
|
192
|
+
attempt('bootstrap-proxy') do
|
|
193
|
+
remove_service(identity('bootstrap-proxy'), @state.fetch('bootstrap_proxy').fetch('id'))
|
|
194
|
+
@state.delete('bootstrap_proxy')
|
|
195
|
+
save
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# rubocop:disable-next Metrics/AbcSize -- Preserve gateway dependencies while continuing independent service cleanup.
|
|
200
|
+
def remove_services
|
|
201
|
+
records = plane.fetch('services', {})
|
|
202
|
+
keys = (records.keys - %w[proxy dns gateway]).reverse + %w[proxy dns gateway]
|
|
203
|
+
keys.each do |key|
|
|
204
|
+
next unless records.key?(key)
|
|
205
|
+
next if key == 'gateway' && @state.fetch('nodes', {}).any?
|
|
206
|
+
|
|
207
|
+
attempt("service #{key}") do
|
|
208
|
+
remove_service(identity(key), records.fetch(key).fetch('id'))
|
|
209
|
+
records.delete(key)
|
|
210
|
+
save
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def remove_volumes
|
|
216
|
+
records = plane.fetch('volumes', {})
|
|
217
|
+
records.dup.each_key do |key|
|
|
218
|
+
attempt("volume #{key}") do
|
|
219
|
+
remove_volume(identity(key), records.fetch(key).fetch('id'))
|
|
220
|
+
records.delete(key)
|
|
221
|
+
save
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def remove_volume(definition, id)
|
|
227
|
+
raise StateError, 'Invalid recorded volume ID' unless Node::Inventory.valid_id?(id)
|
|
228
|
+
|
|
229
|
+
resource = @runtime.inspect_volume(definition, expected_id: id)
|
|
230
|
+
return unless resource
|
|
231
|
+
|
|
232
|
+
raise Providers::OwnershipError, 'Volume has no recorded instance ID; resource retained' unless id
|
|
233
|
+
|
|
234
|
+
@runtime.remove_volume(definition, expected_id: id)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# rubocop:disable-next Metrics/AbcSize -- Independent network teardown follows confirmed node and bridge removal.
|
|
238
|
+
def remove_networks
|
|
239
|
+
if @state.fetch('nodes', {}).any?
|
|
240
|
+
@errors << 'Shared network and gateway retained while node cleanup is incomplete'
|
|
241
|
+
return
|
|
242
|
+
end
|
|
243
|
+
remove_bridge
|
|
244
|
+
attempt('egress network') do
|
|
245
|
+
definition = Network::Egress.new(workspace: @context.workspace, policy: Network::Policy.new)
|
|
246
|
+
remove_network(definition, plane['egress'])
|
|
247
|
+
plane['egress'] = nil if @state.key?('control_plane')
|
|
248
|
+
save
|
|
249
|
+
end
|
|
250
|
+
attempt('environment network') { remove_environment_network }
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# rubocop:disable-next Metrics/AbcSize -- Check recorded logical identity before native network removal.
|
|
254
|
+
def remove_environment_network
|
|
255
|
+
raise StateError, 'Workspace bridge cleanup is incomplete; network retained' if plane['bridge_prepared']
|
|
256
|
+
|
|
257
|
+
entry = @state.fetch('resources').fetch('network')
|
|
258
|
+
definition = Network::Definition.new(workspace: @context.workspace, policy: Network::Policy.new)
|
|
259
|
+
unless [nil, definition.identity].include?(entry['logical_identity']) &&
|
|
260
|
+
[nil, definition.backend_name].include?(entry['name'])
|
|
261
|
+
raise Providers::OwnershipError, 'Recorded network identity is inconsistent; network retained'
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
remove_network(definition, entry['id'])
|
|
265
|
+
entry['id'] = nil
|
|
266
|
+
save
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# rubocop:disable-next Metrics/AbcSize, Metrics/MethodLength -- Native bridge teardown uses only recorded owner, ID and allocation.
|
|
270
|
+
def remove_bridge
|
|
271
|
+
return unless plane['bridge_prepared']
|
|
272
|
+
|
|
273
|
+
attempt('workspace bridge') do
|
|
274
|
+
definition = Network::Definition.new(workspace: @context.workspace, policy: Network::Policy.new)
|
|
275
|
+
id = @state.dig('resources', 'network', 'id')
|
|
276
|
+
resource = @runtime.inspect_network(identifier: definition.backend_name)
|
|
277
|
+
if resource
|
|
278
|
+
definition.verify_ownership!(resource, expected_id: id)
|
|
279
|
+
raise Providers::OwnershipError, 'Bridge has no recorded network ID' unless id
|
|
280
|
+
|
|
281
|
+
definition.allocation = @state.fetch('peer_network').fetch('subnet')
|
|
282
|
+
@runtime.reconcile_workspace_bridge(definition: definition, expected_id: id,
|
|
283
|
+
image: plane.fetch('bridge_image'), action: 'remove')
|
|
284
|
+
end
|
|
285
|
+
plane.delete('bridge_prepared')
|
|
286
|
+
plane.delete('bridge_image')
|
|
287
|
+
save
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def remove_network(definition, id)
|
|
292
|
+
raise StateError, 'Invalid recorded network ID' unless Node::Inventory.valid_id?(id)
|
|
293
|
+
|
|
294
|
+
resource = @runtime.inspect_network(identifier: definition.backend_name)
|
|
295
|
+
definition.verify_ownership!(resource, expected_id: id)
|
|
296
|
+
if resource
|
|
297
|
+
raise Providers::OwnershipError, 'Network has no recorded instance ID; resource retained' unless id
|
|
298
|
+
|
|
299
|
+
@runtime.remove_network(definition: definition, expected_id: id)
|
|
300
|
+
elsif id && @runtime.inspect_network(identifier: id)
|
|
301
|
+
raise Providers::OwnershipError, 'Recorded network ID exists with a different identity; resource retained'
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def reject_unknown_resources
|
|
306
|
+
unknown = @state.fetch('resources').keys - ['network']
|
|
307
|
+
@errors << "Unrecognized resource groups retained: #{unknown.join(', ')}" unless unknown.empty?
|
|
308
|
+
known_plane = %w[services volumes egress bridge_prepared bridge_image stopped environment_cache
|
|
309
|
+
gateway_policy proxy_reload]
|
|
310
|
+
unknown = plane.keys - known_plane
|
|
311
|
+
@errors << "Unrecognized control-plane fields retained: #{unknown.join(', ')}" unless unknown.empty?
|
|
312
|
+
return unless @state.key?('vm_gateway')
|
|
313
|
+
|
|
314
|
+
@errors << 'Legacy VM gateway ownership is not safely interpretable; state retained'
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def cleanup_files
|
|
318
|
+
%w[containers vms services environments additional-configurations puppetdb-relay database.env].each do |name|
|
|
319
|
+
OwnedFiles.remove(@store.directory.join(name), root: @store.directory, symlinks: name == 'environments')
|
|
320
|
+
end
|
|
321
|
+
locks = @store.directory.children.select do |path|
|
|
322
|
+
path.basename.to_s.match?(/\Avm-session-[0-9a-f]{32}\.lock\z/)
|
|
323
|
+
end
|
|
324
|
+
locks.each { |path| OwnedFiles.remove(path, root: @store.directory) }
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module Empeira
|
|
7
|
+
module Infrastructure
|
|
8
|
+
class Definition
|
|
9
|
+
REVISION = 2
|
|
10
|
+
attr_reader :network, :metadata, :plan
|
|
11
|
+
|
|
12
|
+
def self.fingerprint(data)
|
|
13
|
+
Digest::SHA256.hexdigest(JSON.generate(canonical(data)))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.canonical(value)
|
|
17
|
+
case value
|
|
18
|
+
when Hash then value.keys.sort.to_h { |key| [key, canonical(value.fetch(key))] }
|
|
19
|
+
when Array then value.map { |entry| canonical(entry) }
|
|
20
|
+
else value
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def initialize(context:, revision: REVISION)
|
|
25
|
+
@network = Network::Definition.new(
|
|
26
|
+
workspace: context.workspace,
|
|
27
|
+
policy: Network::Policy.new,
|
|
28
|
+
revision: revision
|
|
29
|
+
)
|
|
30
|
+
@metadata = Immutable.deep_freeze(
|
|
31
|
+
{ 'revision' => revision, 'runtime' => context.container_engine,
|
|
32
|
+
'components' => component_definitions(context) }
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# rubocop:disable-next Metrics/AbcSize -- Aggregate fingerprints include dormant browser configuration.
|
|
37
|
+
def component_definitions(context)
|
|
38
|
+
@plan = ControlPlane::Plan.new(context: context)
|
|
39
|
+
components = plan.definitions.merge(ControlPlane::Browser.new(plan).definitions).transform_values(&method(:safe_options))
|
|
40
|
+
components['network'] = network.to_h
|
|
41
|
+
components['egress'] = Network::Egress.new(workspace: context.workspace, policy: Network::Policy.new).to_h
|
|
42
|
+
components
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def safe_options(entry)
|
|
46
|
+
options = entry.options.except('dns', 'runtime_environment', 'runtime_ip')
|
|
47
|
+
options.merge('environment' => self.class.fingerprint(options.fetch('environment', {})))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def fingerprint
|
|
51
|
+
self.class.fingerprint(metadata)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def component_fingerprints
|
|
55
|
+
metadata.fetch('components').transform_values { |definition| self.class.fingerprint(definition) }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Infrastructure
|
|
5
|
+
# Only private workspace descendants are removable; never traverse a symlink.
|
|
6
|
+
module OwnedFiles
|
|
7
|
+
def self.remove(path, root:, symlinks: false)
|
|
8
|
+
return unless verify_removable!(path, root: root, symlinks: symlinks)
|
|
9
|
+
|
|
10
|
+
FileUtils.remove_entry_secure(path)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.verify_removable!(path, root:, symlinks: false)
|
|
14
|
+
verify_root!(root)
|
|
15
|
+
candidate = Pathname(path)
|
|
16
|
+
unless candidate.to_s.start_with?("#{root}/") && candidate.cleanpath == candidate
|
|
17
|
+
raise Providers::OwnershipError, 'Cleanup path escapes its workspace'
|
|
18
|
+
end
|
|
19
|
+
return unless candidate.exist? || candidate.symlink?
|
|
20
|
+
|
|
21
|
+
ancestors(candidate, root).each { |entry| verify_entry!(entry, directory: true) }
|
|
22
|
+
verify_tree!(candidate, symlinks: symlinks)
|
|
23
|
+
true
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.verify_root!(root)
|
|
27
|
+
verify_entry!(root, directory: true)
|
|
28
|
+
raise Providers::OwnershipError, 'Workspace directory is not private' unless root.lstat.mode.nobits?(0o077)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.ancestors(path, root)
|
|
32
|
+
entries = []
|
|
33
|
+
current = path.dirname
|
|
34
|
+
until current == root
|
|
35
|
+
entries << current
|
|
36
|
+
current = current.dirname
|
|
37
|
+
end
|
|
38
|
+
entries.reverse
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.verify_tree!(path, symlinks: false)
|
|
42
|
+
if symlinks && path.symlink?
|
|
43
|
+
raise Providers::OwnershipError, 'Generated link ownership changed' unless path.lstat.uid == Process.uid
|
|
44
|
+
|
|
45
|
+
return
|
|
46
|
+
end
|
|
47
|
+
verify_entry!(path)
|
|
48
|
+
return unless path.directory?
|
|
49
|
+
|
|
50
|
+
path.children.each { |child| verify_tree!(child, symlinks: symlinks) }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# rubocop:disable-next Metrics/CyclomaticComplexity -- File kind, link and owner checks form one fail-closed predicate.
|
|
54
|
+
def self.verify_entry!(path, directory: false)
|
|
55
|
+
metadata = path.lstat
|
|
56
|
+
unless metadata.uid == Process.uid && !metadata.symlink? &&
|
|
57
|
+
(!directory || metadata.directory?) && (metadata.file? || metadata.directory? || metadata.socket?)
|
|
58
|
+
raise Providers::OwnershipError, 'Unsafe workspace path or ownership; cleanup refused'
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module Infrastructure
|
|
5
|
+
# Recovery reads ownership, never migrates schemas or interprets guest configuration.
|
|
6
|
+
module RecoveryInventory
|
|
7
|
+
def self.validate!(data, workspace:)
|
|
8
|
+
return if valid_envelope?(data, workspace) && valid_retired_certificates?(data)
|
|
9
|
+
|
|
10
|
+
raise StateError, 'Recovery inventory ownership or resource envelope is invalid; preserve the state'
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity -- Validate the complete recoverable ownership envelope.
|
|
14
|
+
def self.valid_envelope?(data, workspace)
|
|
15
|
+
data.is_a?(Hash) && data['workspace'] == workspace &&
|
|
16
|
+
data['schema_version'].is_a?(Integer) && data['schema_version'].positive? &&
|
|
17
|
+
Runtime.registry.names.include?(data['runtime']) && data['resources'].is_a?(Hash) &&
|
|
18
|
+
data.fetch('nodes', {}).is_a?(Hash) && valid_plane?(data.fetch('control_plane', {}))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.valid_plane?(plane)
|
|
22
|
+
plane.is_a?(Hash) && %w[services volumes].all? { |key| plane.fetch(key, {}).is_a?(Hash) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.valid_retired_certificates?(data)
|
|
26
|
+
entries = data.fetch('retired_certificates', {})
|
|
27
|
+
entries.is_a?(Hash) && entries.all? do |name, key|
|
|
28
|
+
Node::Inventory.valid_name?(name) && key.is_a?(String) && key.match?(/\A[0-9a-f]{64}\z/)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# rubocop:disable-next Metrics/CyclomaticComplexity -- Validate identity before dispatching numeric-root host cleanup.
|
|
33
|
+
def self.node!(name, record)
|
|
34
|
+
unless Node::Inventory.valid_name?(name) && record.is_a?(Hash) && record['hostname'] == name &&
|
|
35
|
+
%w[container vm].include?(record['provider']) && Node::Inventory.valid_id?(record['id'])
|
|
36
|
+
raise StateError, 'Node identity is not safely interpretable; resources retained'
|
|
37
|
+
end
|
|
38
|
+
return if record['provider'] == 'container'
|
|
39
|
+
|
|
40
|
+
return if valid_vm_instance?(name, record)
|
|
41
|
+
|
|
42
|
+
raise StateError, 'VM instance or disk identity is incomplete; resources retained'
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.valid_vm_instance?(name, record)
|
|
46
|
+
peer = record['peer']
|
|
47
|
+
valid_token = peer.is_a?(Hash) && peer['token'].is_a?(String) && peer['token'].match?(/\A[0-9a-f]{32}\z/)
|
|
48
|
+
valid_pid = record['pid'].nil? || (record['pid'].is_a?(Integer) && record['pid'].positive?)
|
|
49
|
+
valid_token && valid_pid && record['overlay'] == "vms/#{name}/disk.qcow2"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|