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,240 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Empeira
|
|
6
|
+
module VM
|
|
7
|
+
# Intent is saved before netlink changes; uncertain transport outcomes are inspected on retry.
|
|
8
|
+
# rubocop:disable-next Metrics/ClassLength -- One locked transaction owns device and parent recovery.
|
|
9
|
+
class Interfaces
|
|
10
|
+
PARENT = Configuration::VMInterfaces::PARENT
|
|
11
|
+
|
|
12
|
+
def initialize(context:, record:, state:, guest:, persist:)
|
|
13
|
+
@context = context
|
|
14
|
+
@record = record
|
|
15
|
+
@state = state
|
|
16
|
+
@persist = persist
|
|
17
|
+
@guest = GuestNetwork.new(guest: guest, record: record)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.desired(context, hostname)
|
|
21
|
+
Configuration::VMInterfaces.resolve(context.configuration.dig('vm', 'interfaces'), hostname)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.validate_static!(context, hostname, state)
|
|
25
|
+
definitions = desired(context, hostname)
|
|
26
|
+
InterfaceRoutes.validate_static!(definitions, hostname: hostname, config: context.configuration,
|
|
27
|
+
subnet: state&.dig('peer_network', 'subnet'))
|
|
28
|
+
definitions
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.valid_inventory?(inventory)
|
|
32
|
+
return false unless valid_envelope?(inventory)
|
|
33
|
+
|
|
34
|
+
inventory['devices'].all? { |name, entry| valid_entry?(name, entry) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.valid_envelope?(inventory)
|
|
38
|
+
inventory.is_a?(Hash) && inventory.keys.sort_by(&:to_s) == %w[devices parent token] &&
|
|
39
|
+
inventory['token'].is_a?(String) && inventory['token'].match?(/\A[a-f0-9]{32}\z/) &&
|
|
40
|
+
[true, false].include?(inventory['parent']) && inventory['devices'].is_a?(Hash)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.valid_entry?(name, entry)
|
|
44
|
+
return false unless entry.is_a?(Hash) && entry.key?('definition') &&
|
|
45
|
+
(entry.keys - %w[definition previous]).empty?
|
|
46
|
+
|
|
47
|
+
[entry.fetch('definition'), *entry.values_at('previous').compact].each do |definition|
|
|
48
|
+
Configuration::VMInterfaces.device!(name, definition, 'vm.interfaces inventory')
|
|
49
|
+
end
|
|
50
|
+
true
|
|
51
|
+
rescue ConfigurationError
|
|
52
|
+
false
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# rubocop:disable-next Metrics/AbcSize -- Ordered mutations retain inspectable intent on every failure.
|
|
56
|
+
def reconcile
|
|
57
|
+
@desired = self.class.validate_static!(@context, @record.fetch('hostname'), @state)
|
|
58
|
+
return false if @desired.empty? && !@record.key?('network_interfaces')
|
|
59
|
+
|
|
60
|
+
prepare_inventory
|
|
61
|
+
observe!
|
|
62
|
+
defaults = @guest.defaults
|
|
63
|
+
@changed = false
|
|
64
|
+
prepare_intents
|
|
65
|
+
prune
|
|
66
|
+
vlan? ? ensure_parent : remove_parent
|
|
67
|
+
@desired.each { |name, definition| apply(name, definition) }
|
|
68
|
+
verify!(defaults)
|
|
69
|
+
@desired.empty? ? release_inventory : @guest.verify_facter!(@desired)
|
|
70
|
+
@changed
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def observe!
|
|
76
|
+
@guest.load_modules(vlan: vlan?) unless @desired.empty?
|
|
77
|
+
@guest.refresh
|
|
78
|
+
names = (@inventory.fetch('devices').keys + @desired.keys + (parent_needed? ? [PARENT] : [])).uniq
|
|
79
|
+
@ownership.validate!(names)
|
|
80
|
+
InterfaceRoutes.new(guest: @guest, ownership: @ownership,
|
|
81
|
+
hostname: @record.fetch('hostname')).validate!(@desired)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def release_inventory
|
|
85
|
+
@record.delete('network_interfaces')
|
|
86
|
+
@persist.call
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def prepare_inventory
|
|
90
|
+
unless @record.key?('network_interfaces')
|
|
91
|
+
@record['network_interfaces'] = { 'token' => SecureRandom.hex(16), 'parent' => false, 'devices' => {} }
|
|
92
|
+
@persist.call
|
|
93
|
+
end
|
|
94
|
+
@inventory = @record.fetch('network_interfaces')
|
|
95
|
+
@ownership = InterfaceOwnership.new(context: @context, record: @record, inventory: @inventory, guest: @guest)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def vlan?
|
|
99
|
+
@desired.values.any? { |definition| definition.key?('vlan_id') }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def parent_needed?
|
|
103
|
+
vlan? || @inventory.fetch('parent')
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def prune
|
|
107
|
+
# Snapshot names because confirmed removals change this inventory.
|
|
108
|
+
@inventory.fetch('devices').keys.each do |name| # rubocop:disable Style/HashEachMethods
|
|
109
|
+
if @desired.key?(name)
|
|
110
|
+
observed = @ownership.observed_definition(name)
|
|
111
|
+
delete_link(name) if observed && observed != @desired.fetch(name)
|
|
112
|
+
next
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
delete_link(name)
|
|
116
|
+
@inventory.fetch('devices').delete(name)
|
|
117
|
+
@persist.call
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def prepare_intents
|
|
122
|
+
@desired.each do |name, definition|
|
|
123
|
+
entry = @inventory.fetch('devices')[name]
|
|
124
|
+
next if entry && entry.fetch('definition') == definition
|
|
125
|
+
|
|
126
|
+
observed = @ownership.observed_definition(name)
|
|
127
|
+
@inventory.fetch('devices')[name] = { 'definition' => definition,
|
|
128
|
+
**(observed ? { 'previous' => observed } : {}) }
|
|
129
|
+
@persist.call
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def ensure_parent
|
|
134
|
+
unless @inventory.fetch('parent')
|
|
135
|
+
@inventory['parent'] = true
|
|
136
|
+
@persist.call
|
|
137
|
+
end
|
|
138
|
+
create_link(PARENT, {}) unless @guest.links.key?(PARENT)
|
|
139
|
+
activate(PARENT)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def remove_parent
|
|
143
|
+
return unless @inventory.fetch('parent')
|
|
144
|
+
|
|
145
|
+
delete_link(PARENT)
|
|
146
|
+
@inventory['parent'] = false
|
|
147
|
+
@persist.call
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def apply(name, definition)
|
|
151
|
+
entry = @inventory.fetch('devices').fetch(name)
|
|
152
|
+
create_link(name, definition) unless @guest.links.key?(name)
|
|
153
|
+
mark_link(name, definition)
|
|
154
|
+
configure_address(name, definition)
|
|
155
|
+
activate(name)
|
|
156
|
+
@persist.call if entry.delete('previous')
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def create_link(name, definition)
|
|
160
|
+
# Linux applies IFLA_ADDRESS on creation; IFLA_IFALIAS needs a subsequent setlink.
|
|
161
|
+
arguments = ['ip', 'link', 'add', 'name', name, 'address', @ownership.address(name, definition)]
|
|
162
|
+
arguments += if definition.key?('vlan_id')
|
|
163
|
+
['link', PARENT, 'type', 'vlan', 'id', definition.fetch('vlan_id').to_s]
|
|
164
|
+
else
|
|
165
|
+
%w[type dummy]
|
|
166
|
+
end
|
|
167
|
+
change(arguments)
|
|
168
|
+
@ownership.observed_definition(name)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def mark_link(name, definition)
|
|
172
|
+
@ownership.observed_definition(name)
|
|
173
|
+
marker = @ownership.marker(name, definition)
|
|
174
|
+
return if @guest.links.fetch(name)['ifalias'] == marker
|
|
175
|
+
|
|
176
|
+
change(['ip', 'link', 'set', 'dev', name, 'alias', marker])
|
|
177
|
+
return if @guest.links.fetch(name)['ifalias'] == marker
|
|
178
|
+
|
|
179
|
+
@ownership.conflict!(name, 'ownership alias could not be verified')
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def configure_address(name, definition)
|
|
183
|
+
return unless @guest.ipv4(name).empty?
|
|
184
|
+
|
|
185
|
+
change(['ip', 'address', 'add', definition.fetch('network'), 'dev', name])
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def activate(name)
|
|
189
|
+
mark_link(name, @ownership.observed_definition(name))
|
|
190
|
+
return if @guest.links.fetch(name).fetch('flags').include?('UP')
|
|
191
|
+
|
|
192
|
+
change(['ip', 'link', 'set', 'dev', name, 'up'])
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def delete_link(name)
|
|
196
|
+
@guest.refresh
|
|
197
|
+
return unless @guest.links.key?(name)
|
|
198
|
+
|
|
199
|
+
@ownership.deletable!(name)
|
|
200
|
+
change(['ip', 'link', 'delete', 'dev', name])
|
|
201
|
+
@ownership.conflict!(name, 'deletion could not be verified') if @guest.links.key?(name)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def change(arguments)
|
|
205
|
+
@guest.command(arguments)
|
|
206
|
+
@changed = true
|
|
207
|
+
@guest.refresh
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def verify!(defaults)
|
|
211
|
+
@guest.refresh
|
|
212
|
+
unless @guest.defaults == defaults
|
|
213
|
+
raise Error, "Host #{@record.fetch('hostname')}, vm.interfaces: default routes changed; Puppet was not run"
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
@desired.each { |name, definition| verify_device!(name, definition) }
|
|
217
|
+
return unless @inventory.fetch('parent')
|
|
218
|
+
|
|
219
|
+
verify_device!(PARENT, {})
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def verify_device!(name, definition)
|
|
223
|
+
observed = @ownership.observed_definition(name)
|
|
224
|
+
link = @guest.links[name]
|
|
225
|
+
network = definition['network']
|
|
226
|
+
bindings = @guest.ipv4(name).map { |address| "#{address.fetch('local')}/#{address.fetch('prefixlen')}" }
|
|
227
|
+
return if observed == definition && active_link?(name, link, definition) &&
|
|
228
|
+
bindings == (network ? [network] : [])
|
|
229
|
+
|
|
230
|
+
raise Error, "Host #{@record.fetch('hostname')}, interface #{name}, vm.interfaces: " \
|
|
231
|
+
'guest type, address or active state verification failed; Puppet was not run'
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def active_link?(name, link, definition)
|
|
235
|
+
link.fetch('flags').include?('UP') && link['ifalias'] == @ownership.marker(name, definition) &&
|
|
236
|
+
%w[UP UNKNOWN].include?(link['operstate'])
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module VM
|
|
5
|
+
# Numeric-root execution over a private VirtIO port, independent of guest login policy.
|
|
6
|
+
module Management
|
|
7
|
+
VERSION = 2
|
|
8
|
+
SLOTS = 8
|
|
9
|
+
CHANNEL = 'org.empeira.management.0'
|
|
10
|
+
DEVICE = "/dev/virtio-ports/#{CHANNEL}".freeze
|
|
11
|
+
DIRECTORY = '/etc/empeira/management'
|
|
12
|
+
UNIT = '/etc/systemd/system/empeira-management.service'
|
|
13
|
+
AGENT = '/usr/local/libexec/empeira-management-agent'
|
|
14
|
+
SHELL_RC = '/usr/local/libexec/empeira-management-shell-rc'
|
|
15
|
+
SETUP = '/usr/local/libexec/empeira-management-setup'
|
|
16
|
+
UPLOADS = '/run/empeira-management'
|
|
17
|
+
|
|
18
|
+
def self.validate!(record)
|
|
19
|
+
return if record['management_layout'] == VERSION
|
|
20
|
+
|
|
21
|
+
raise Error, 'Incompatible VM management layout; use node destroy to remove the owned VM, then recreate it'
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# rubocop:disable-next Metrics/AbcSize -- Seed the service, adapter and one-time regular account together.
|
|
25
|
+
def self.files(record, system_key:)
|
|
26
|
+
validate!(record)
|
|
27
|
+
raise Error, 'VM system SSH requires a public key' if system_key.to_s.empty?
|
|
28
|
+
|
|
29
|
+
setup = File.read(resource('setup.sh')).sub('@SYSTEM_SETUP@', File.read(resource('system_account.sh')) +
|
|
30
|
+
File.read(resource('system_home.sh')))
|
|
31
|
+
{ AGENT => [File.read(resource('guest_agent.py')), '0755'], UNIT => [service(record), '0644'],
|
|
32
|
+
SETUP => [setup, '0755'], SHELL_RC => [File.read(resource('shell_rc.sh')), '0644'],
|
|
33
|
+
"#{DIRECTORY}/system_authorized_keys" => ["#{system_key}\n", '0644'] }.map do |path, entry|
|
|
34
|
+
{ 'path' => path, 'permissions' => entry[1], 'owner' => 'root:root', 'content' => entry[0] }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# rubocop:disable-next Metrics/MethodLength -- Keep the complete private service policy in one literal.
|
|
39
|
+
def self.service(record)
|
|
40
|
+
token = record.fetch('peer').fetch('token')
|
|
41
|
+
raise Error, 'Invalid VM instance identity' unless token.match?(/\A[0-9a-f]{32}\z/)
|
|
42
|
+
|
|
43
|
+
<<~UNIT
|
|
44
|
+
# Managed by Empeira; no SSH, PAM, NSS login or sudo dependency.
|
|
45
|
+
[Unit]
|
|
46
|
+
Description=Empeira private VirtIO management
|
|
47
|
+
After=systemd-udev-trigger.service
|
|
48
|
+
[Service]
|
|
49
|
+
Type=simple
|
|
50
|
+
User=0
|
|
51
|
+
Group=0
|
|
52
|
+
UMask=0077
|
|
53
|
+
RuntimeDirectory=empeira-management
|
|
54
|
+
RuntimeDirectoryMode=0700
|
|
55
|
+
ExecStart=/usr/local/libexec/empeira-management-python #{AGENT} #{DEVICE} #{token} #{extra_devices.join(' ')}
|
|
56
|
+
StandardOutput=null
|
|
57
|
+
Restart=on-failure
|
|
58
|
+
RestartSec=2
|
|
59
|
+
[Install]
|
|
60
|
+
WantedBy=multi-user.target
|
|
61
|
+
UNIT
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.resource(name)
|
|
65
|
+
Pathname(__dir__).join('../../../resources/nodes/management', name)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.extra_devices
|
|
69
|
+
(1...SLOTS).map { |slot| "/dev/virtio-ports/org.empeira.management.#{slot}" }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module VM
|
|
5
|
+
class Prerequisites
|
|
6
|
+
def initialize(engine:, platform:)
|
|
7
|
+
@engine = engine
|
|
8
|
+
@platform = platform
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def verify!(progress: Progress.new, seeds: true)
|
|
12
|
+
progress.stage(5, 'Checking VM host prerequisites...')
|
|
13
|
+
results = @engine.required_tools(seeds: seeds).to_h do |tool|
|
|
14
|
+
[tool, @engine.find(tool) ? 'available' : 'missing']
|
|
15
|
+
end
|
|
16
|
+
progress.stage(10, 'Checking hardware acceleration and firmware...')
|
|
17
|
+
accelerator = check(results, 'Accelerator') { @engine.accelerator }
|
|
18
|
+
check(results, 'ARM64 firmware') { @engine.firmware || 'not required' }
|
|
19
|
+
return accelerator if results.values.all? { |value| value == 'available' }
|
|
20
|
+
|
|
21
|
+
raise UnavailableFeature, failure_report(results)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def failure_report(results)
|
|
27
|
+
report = results.map { |name, status| " #{name.ljust(23)} #{status}" }.join("\n")
|
|
28
|
+
"VM prerequisites are not satisfied (#{@platform.os}/#{@platform.architecture}):\n\n" \
|
|
29
|
+
"#{report}\n\n#{@platform.vm_install_hint}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def check(results, name)
|
|
33
|
+
value = yield
|
|
34
|
+
results[name] = 'available'
|
|
35
|
+
value
|
|
36
|
+
rescue Error => e
|
|
37
|
+
results[name] = e.message
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pathname'
|
|
4
|
+
|
|
5
|
+
module Empeira
|
|
6
|
+
module VM
|
|
7
|
+
# Host capability detection belongs to the VM engine, not the public node provider.
|
|
8
|
+
class Qemu < Interface
|
|
9
|
+
EXECUTABLES = { amd64: 'qemu-system-x86_64', arm64: 'qemu-system-aarch64' }.freeze
|
|
10
|
+
|
|
11
|
+
def initialize(context:, runner:, executable_path: ENV.fetch('PATH', ''))
|
|
12
|
+
super(name: 'qemu', context: context, runner: runner)
|
|
13
|
+
@executable_path = executable_path
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def executable
|
|
17
|
+
name = EXECUTABLES.fetch(context.platform.architecture)
|
|
18
|
+
find(name) || raise(UnavailableFeature, "#{name} is required for VM nodes; install QEMU")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def image_tool
|
|
22
|
+
find('qemu-img') || raise(UnavailableFeature, 'qemu-img is required for VM disks; install QEMU')
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def accelerator
|
|
26
|
+
desired = desired_accelerator
|
|
27
|
+
Platform::Kvm.new(platform: context.platform).verify! if desired == 'kvm'
|
|
28
|
+
result = runner.run(executable, arguments: ['-accel', 'help'], timeout: 10)
|
|
29
|
+
return desired if result.success? && result.stdout.lines.any? { |line| line.strip == desired }
|
|
30
|
+
|
|
31
|
+
raise UnavailableFeature, "QEMU does not advertise #{desired.upcase} acceleration; no automatic TCG fallback"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def available?
|
|
35
|
+
executable && image_tool && accelerator
|
|
36
|
+
true
|
|
37
|
+
rescue UnavailableFeature
|
|
38
|
+
false
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def desired_accelerator
|
|
44
|
+
case context.platform.os
|
|
45
|
+
when :linux, :wsl then 'kvm'
|
|
46
|
+
when :macos then 'hvf'
|
|
47
|
+
else raise UnsupportedPlatform, 'QEMU VM nodes require Linux or macOS'
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
public
|
|
52
|
+
|
|
53
|
+
def required_tools(seeds: true)
|
|
54
|
+
[EXECUTABLES.fetch(context.platform.architecture), 'qemu-img', 'ssh',
|
|
55
|
+
*(seeds ? %w[xorriso ssh-keygen] : [])]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def preflight!(progress: Progress.new, seeds: true)
|
|
59
|
+
Prerequisites.new(engine: self, platform: context.platform).verify!(progress: progress, seeds: seeds)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def firmware
|
|
63
|
+
return unless context.platform.architecture == :arm64
|
|
64
|
+
|
|
65
|
+
firmware_candidates.find { |path| path.file? && path.readable? } ||
|
|
66
|
+
raise(UnavailableFeature, 'AArch64 UEFI firmware is required with QEMU')
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def firmware_candidates
|
|
70
|
+
root = Pathname(executable).realpath.dirname.parent.join('share')
|
|
71
|
+
[root.join('qemu/edk2-aarch64-code.fd'), root.join('qemu/QEMU_EFI.fd'),
|
|
72
|
+
root.join('qemu-efi-aarch64/QEMU_EFI.fd')]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def find(name)
|
|
76
|
+
@executable_path.split(File::PATH_SEPARATOR).filter_map do |directory|
|
|
77
|
+
path = Pathname(directory).join(name)
|
|
78
|
+
path.to_s if path.file? && path.executable?
|
|
79
|
+
end.first
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|