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,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'pathname'
|
|
5
|
+
require 'tempfile'
|
|
6
|
+
require 'fileutils'
|
|
7
|
+
|
|
8
|
+
module Empeira
|
|
9
|
+
# Runs inside the owned guest using its agent Ruby; no separate guest inventory.
|
|
10
|
+
class ManagedFile
|
|
11
|
+
def initialize(request)
|
|
12
|
+
@path = Pathname(request.fetch('path'))
|
|
13
|
+
@content = request['content']
|
|
14
|
+
@accepted = request.fetch('accepted')
|
|
15
|
+
@mode = request.fetch('mode')
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# rubocop:disable-next Naming/PredicateMethod -- Reconciliation mutates the managed file.
|
|
19
|
+
def reconcile
|
|
20
|
+
verify_parents!
|
|
21
|
+
current = verify_file!
|
|
22
|
+
return false if current.nil? && @content.nil?
|
|
23
|
+
return false if current && current == @content && (@path.stat.mode & 0o7777) == @mode
|
|
24
|
+
|
|
25
|
+
if @content.nil?
|
|
26
|
+
@path.unlink
|
|
27
|
+
else
|
|
28
|
+
write
|
|
29
|
+
end
|
|
30
|
+
true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def verify_parents!
|
|
36
|
+
@path.parent.ascend do |parent|
|
|
37
|
+
next unless parent.exist? || parent.symlink?
|
|
38
|
+
next if !parent.symlink? && parent.directory?
|
|
39
|
+
|
|
40
|
+
raise 'Managed file parent must be a directory, not a symlink'
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def verify_file!
|
|
45
|
+
stat = @path.lstat
|
|
46
|
+
@content ? current_for_write(stat) : current_for_removal(stat)
|
|
47
|
+
rescue Errno::ENOENT
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def current_for_write(stat)
|
|
52
|
+
raise 'Command-mock target must be a file or symlink' unless stat.file? || stat.symlink?
|
|
53
|
+
|
|
54
|
+
return nil if stat.symlink? || stat.size != @content.bytesize
|
|
55
|
+
|
|
56
|
+
@path.binread
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def current_for_removal(stat)
|
|
60
|
+
unless stat.file? && stat.nlink == 1 && stat.uid == Process.euid
|
|
61
|
+
raise 'Refusing to remove an unowned or modified command-mock target'
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
content = @path.binread
|
|
65
|
+
raise 'Refusing to remove an unowned or modified command-mock target' unless @accepted.include?(content)
|
|
66
|
+
|
|
67
|
+
content
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def write
|
|
71
|
+
FileUtils.mkdir_p(@path.parent, mode: 0o755)
|
|
72
|
+
Tempfile.create('.empeira-', @path.parent) do |file|
|
|
73
|
+
file.chmod(@mode)
|
|
74
|
+
file.write(@content)
|
|
75
|
+
file.flush
|
|
76
|
+
file.fsync
|
|
77
|
+
File.rename(file.path, @path)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
if $PROGRAM_NAME == __FILE__
|
|
84
|
+
begin
|
|
85
|
+
changed = Empeira::ManagedFile.new(JSON.parse(ARGV.fetch(0))).reconcile
|
|
86
|
+
puts JSON.generate('changed' => changed)
|
|
87
|
+
rescue StandardError => e
|
|
88
|
+
warn e.message
|
|
89
|
+
exit 1
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
# SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
"""Private VirtIO-Serial execution adapter; no account authentication or network."""
|
|
4
|
+
import base64
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import select
|
|
9
|
+
import signal
|
|
10
|
+
import shutil
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
import stat
|
|
14
|
+
import tempfile
|
|
15
|
+
import time
|
|
16
|
+
import errno
|
|
17
|
+
import fcntl
|
|
18
|
+
import pty
|
|
19
|
+
import pwd
|
|
20
|
+
import struct
|
|
21
|
+
import termios
|
|
22
|
+
|
|
23
|
+
LIMIT = 131072
|
|
24
|
+
CHUNK = 16384
|
|
25
|
+
TOKEN = sys.argv[2]
|
|
26
|
+
PORT = sys.argv[1]
|
|
27
|
+
ROOT = '/run/empeira-management'
|
|
28
|
+
os.umask(0o077)
|
|
29
|
+
os.makedirs(ROOT, mode=0o700, exist_ok=True)
|
|
30
|
+
metadata = os.lstat(ROOT)
|
|
31
|
+
if not stat.S_ISDIR(metadata.st_mode) or metadata.st_uid != 0 or metadata.st_mode & 0o077:
|
|
32
|
+
raise RuntimeError('Management staging ownership is invalid')
|
|
33
|
+
if os.geteuid() != 0:
|
|
34
|
+
raise RuntimeError('Management requires numeric UID 0')
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def interrupted(signum, _frame):
|
|
38
|
+
raise SystemExit(128 + signum)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
for termination_signal in (signal.SIGTERM, signal.SIGINT, signal.SIGHUP):
|
|
42
|
+
signal.signal(termination_signal, interrupted)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def supervise(ports):
|
|
46
|
+
# Independent bounded workers preserve the existing streaming/atomic-upload protocol.
|
|
47
|
+
# A blocked or failed session cannot stall another channel or ordinary Puppet execution.
|
|
48
|
+
workers = {}
|
|
49
|
+
try:
|
|
50
|
+
while True:
|
|
51
|
+
for port in ports:
|
|
52
|
+
worker = workers.get(port)
|
|
53
|
+
if worker is None or worker.poll() is not None:
|
|
54
|
+
workers[port] = subprocess.Popen([sys.executable, __file__, port, TOKEN])
|
|
55
|
+
time.sleep(0.5)
|
|
56
|
+
finally:
|
|
57
|
+
for worker in workers.values():
|
|
58
|
+
if worker.poll() is None:
|
|
59
|
+
worker.terminate()
|
|
60
|
+
for worker in workers.values():
|
|
61
|
+
try:
|
|
62
|
+
worker.wait(timeout=3)
|
|
63
|
+
except subprocess.TimeoutExpired:
|
|
64
|
+
worker.kill()
|
|
65
|
+
worker.wait()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if len(sys.argv) > 3:
|
|
69
|
+
supervise([PORT, *sys.argv[3:]])
|
|
70
|
+
raise SystemExit(0)
|
|
71
|
+
|
|
72
|
+
fd = os.open(PORT, os.O_RDWR | os.O_NONBLOCK)
|
|
73
|
+
events = select.epoll()
|
|
74
|
+
events.register(fd, select.EPOLLIN | select.EPOLLET | select.EPOLLHUP)
|
|
75
|
+
buffer = bytearray()
|
|
76
|
+
active = None
|
|
77
|
+
request_id = None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def send(data):
|
|
81
|
+
data['id'] = request_id
|
|
82
|
+
raw = (json.dumps(data, separators=(',', ':')) + '\n').encode()
|
|
83
|
+
if len(raw) > LIMIT:
|
|
84
|
+
raise ValueError('Oversized protocol frame')
|
|
85
|
+
deadline = time.monotonic() + 5
|
|
86
|
+
while raw:
|
|
87
|
+
if time.monotonic() >= deadline:
|
|
88
|
+
raise TimeoutError()
|
|
89
|
+
try:
|
|
90
|
+
written = os.write(fd, raw)
|
|
91
|
+
if written <= 0:
|
|
92
|
+
raise EOFError()
|
|
93
|
+
raw = raw[written:]
|
|
94
|
+
except BlockingIOError:
|
|
95
|
+
select.select([], [fd], [], 2)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def session_groups(pid):
|
|
99
|
+
groups = set()
|
|
100
|
+
for entry in os.scandir('/proc'):
|
|
101
|
+
if not entry.name.isdigit():
|
|
102
|
+
continue
|
|
103
|
+
try:
|
|
104
|
+
with open(entry.path + '/stat') as source:
|
|
105
|
+
fields = source.read().rpartition(') ')[2].split()
|
|
106
|
+
if fields[0] != 'Z' and int(fields[3]) == pid:
|
|
107
|
+
groups.add(int(fields[2]))
|
|
108
|
+
except (FileNotFoundError, ProcessLookupError):
|
|
109
|
+
continue
|
|
110
|
+
return groups
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def terminate_groups(pid):
|
|
114
|
+
# The unreaped session leader pins the identity even when its shell created job-control groups.
|
|
115
|
+
groups = session_groups(pid)
|
|
116
|
+
if not groups:
|
|
117
|
+
return
|
|
118
|
+
for sig in (signal.SIGTERM, signal.SIGKILL):
|
|
119
|
+
for group in groups:
|
|
120
|
+
try:
|
|
121
|
+
os.killpg(group, sig)
|
|
122
|
+
except ProcessLookupError:
|
|
123
|
+
pass
|
|
124
|
+
if sig == signal.SIGTERM:
|
|
125
|
+
time.sleep(0.1)
|
|
126
|
+
groups = session_groups(pid)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def terminate():
|
|
130
|
+
global active
|
|
131
|
+
if not active:
|
|
132
|
+
return
|
|
133
|
+
if active['kind'] in ('exec', 'shell'):
|
|
134
|
+
process = active['process']
|
|
135
|
+
# The unreaped session leader pins the PID/process-group until cleanup.
|
|
136
|
+
terminate_groups(process.pid)
|
|
137
|
+
process.wait()
|
|
138
|
+
for pipe in active['pipes']:
|
|
139
|
+
if isinstance(pipe, int) and pipe not in active['open']:
|
|
140
|
+
continue
|
|
141
|
+
if not isinstance(pipe, int) and pipe.closed:
|
|
142
|
+
continue
|
|
143
|
+
try:
|
|
144
|
+
events.unregister(pipe if isinstance(pipe, int) else pipe.fileno())
|
|
145
|
+
except (FileNotFoundError, OSError):
|
|
146
|
+
pass
|
|
147
|
+
os.close(pipe) if isinstance(pipe, int) else pipe.close()
|
|
148
|
+
else:
|
|
149
|
+
active['file'].close()
|
|
150
|
+
os.unlink(active['path'])
|
|
151
|
+
active = None
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def start_exec(message):
|
|
155
|
+
global active
|
|
156
|
+
argv = message['argv']
|
|
157
|
+
if not isinstance(argv, list) or not argv or not all(isinstance(s, str) and '\0' not in s for s in argv):
|
|
158
|
+
raise ValueError('Invalid command arguments')
|
|
159
|
+
try:
|
|
160
|
+
process = subprocess.Popen(argv, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
|
|
161
|
+
stderr=subprocess.PIPE, start_new_session=True,
|
|
162
|
+
env={**{key: value for key, value in os.environ.items() if key != 'RUNTIME_DIRECTORY'}, 'PATH': '/opt/puppetlabs/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'})
|
|
163
|
+
except OSError as error:
|
|
164
|
+
send({'type': 'stderr', 'data': base64.b64encode((os.strerror(error.errno) + '\n').encode()).decode()})
|
|
165
|
+
send({'type': 'exit', 'status': 127 if error.errno == 2 else 126, 'timed_out': False})
|
|
166
|
+
return
|
|
167
|
+
pipes = {process.stdout.fileno(): (process.stdout, 'stdout'),
|
|
168
|
+
process.stderr.fileno(): (process.stderr, 'stderr')}
|
|
169
|
+
active = {'kind': 'exec', 'process': process, 'pipes': [process.stdout, process.stderr],
|
|
170
|
+
'open': pipes, 'deadline': time.monotonic() + message['timeout'] if message['timeout'] else None}
|
|
171
|
+
for pipe in pipes:
|
|
172
|
+
os.set_blocking(pipe, False)
|
|
173
|
+
events.register(pipe, select.EPOLLIN | select.EPOLLHUP)
|
|
174
|
+
send({'type': 'started'})
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class ShellProcess:
|
|
178
|
+
def __init__(self, pid):
|
|
179
|
+
self.pid = pid
|
|
180
|
+
|
|
181
|
+
def wait(self):
|
|
182
|
+
_, status = os.waitpid(self.pid, 0)
|
|
183
|
+
return os.WEXITSTATUS(status) if os.WIFEXITED(status) else -os.WTERMSIG(status)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def terminal_size(message):
|
|
187
|
+
size = message.get('size')
|
|
188
|
+
if (not isinstance(size, list) or len(size) != 2 or
|
|
189
|
+
not all(type(value) is int and 0 < value <= 65535 for value in size)):
|
|
190
|
+
raise ValueError('Invalid terminal dimensions')
|
|
191
|
+
return struct.pack('HHHH', *size, 0, 0)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def command_environment():
|
|
195
|
+
return {**{key: value for key, value in os.environ.items() if key != 'RUNTIME_DIRECTORY'},
|
|
196
|
+
'PATH': '/opt/puppetlabs/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
SHELL_RC = '/usr/local/libexec/empeira-management-shell-rc'
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def start_shell(message):
|
|
203
|
+
global active
|
|
204
|
+
size = terminal_size(message)
|
|
205
|
+
term = message.get('term', 'xterm')
|
|
206
|
+
if not isinstance(term, str) or not 1 <= len(term) <= 64 or not all(c.isalnum() or c in '-_.+' for c in term):
|
|
207
|
+
raise ValueError('Invalid terminal type')
|
|
208
|
+
shell = shutil.which('bash') or shutil.which('sh')
|
|
209
|
+
if not shell:
|
|
210
|
+
raise ValueError('No guest shell is available')
|
|
211
|
+
pid, master = pty.fork()
|
|
212
|
+
if pid == 0:
|
|
213
|
+
try:
|
|
214
|
+
fcntl.ioctl(0, termios.TIOCSWINSZ, size)
|
|
215
|
+
try:
|
|
216
|
+
home = pwd.getpwuid(0).pw_dir
|
|
217
|
+
except KeyError:
|
|
218
|
+
home = '/'
|
|
219
|
+
os.chdir(home if os.path.isdir(home) else '/')
|
|
220
|
+
arguments = [shell, '--rcfile', SHELL_RC, '-i'] if os.path.basename(shell) == 'bash' else [shell, '-i']
|
|
221
|
+
os.execvpe(shell, arguments, {**command_environment(), 'HOME': home, 'HISTFILE': '/dev/null',
|
|
222
|
+
'USER': 'root', 'LOGNAME': 'root', 'TERM': term})
|
|
223
|
+
except Exception:
|
|
224
|
+
os._exit(126)
|
|
225
|
+
os.set_blocking(master, False)
|
|
226
|
+
active = {'kind': 'shell', 'process': ShellProcess(pid), 'pipes': [master],
|
|
227
|
+
'open': {master: (master, 'stdout')}, 'deadline': None,
|
|
228
|
+
'input': bytearray(), 'master': master}
|
|
229
|
+
events.register(master, select.EPOLLIN | select.EPOLLHUP)
|
|
230
|
+
send({'type': 'started'})
|
|
231
|
+
send({'type': 'ready'})
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def shell_input(message):
|
|
235
|
+
data = base64.b64decode(message['data'], validate=True)
|
|
236
|
+
if len(data) > CHUNK or active['input']:
|
|
237
|
+
raise ValueError('Shell input exceeds its granted credit')
|
|
238
|
+
active['input'].extend(data)
|
|
239
|
+
events.modify(active['master'], select.EPOLLIN | select.EPOLLOUT | select.EPOLLHUP)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def flush_shell_input():
|
|
243
|
+
master = active['master']
|
|
244
|
+
try:
|
|
245
|
+
written = os.write(master, active['input'])
|
|
246
|
+
del active['input'][:written]
|
|
247
|
+
except BlockingIOError:
|
|
248
|
+
return
|
|
249
|
+
if not active['input']:
|
|
250
|
+
events.modify(master, select.EPOLLIN | select.EPOLLHUP)
|
|
251
|
+
send({'type': 'ready'})
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def handle(message):
|
|
255
|
+
global active, request_id
|
|
256
|
+
kind = message['type']
|
|
257
|
+
if kind == 'hello':
|
|
258
|
+
if message.get('token') != TOKEN:
|
|
259
|
+
raise ValueError('Instance identity mismatch')
|
|
260
|
+
if active:
|
|
261
|
+
terminate()
|
|
262
|
+
request_id = message['id']
|
|
263
|
+
send({'type': 'hello', 'token': TOKEN, 'version': 2})
|
|
264
|
+
return
|
|
265
|
+
if message['id'] != request_id:
|
|
266
|
+
raise ValueError('Request identity mismatch')
|
|
267
|
+
if kind == 'cancel':
|
|
268
|
+
terminate()
|
|
269
|
+
send({'type': 'exit', 'status': 130, 'timed_out': False})
|
|
270
|
+
elif kind == 'exec' and active is None:
|
|
271
|
+
timeout = message['timeout']
|
|
272
|
+
if timeout is not None and (type(timeout) not in (int, float) or not 0 < timeout <= 86400):
|
|
273
|
+
raise ValueError('Invalid operation timeout')
|
|
274
|
+
start_exec(message)
|
|
275
|
+
elif kind == 'shell' and active is None:
|
|
276
|
+
start_shell(message)
|
|
277
|
+
elif active and active['kind'] == 'shell' and kind == 'input':
|
|
278
|
+
shell_input(message)
|
|
279
|
+
elif active and active['kind'] == 'shell' and kind == 'resize':
|
|
280
|
+
fcntl.ioctl(active['master'], termios.TIOCSWINSZ, terminal_size(message))
|
|
281
|
+
elif active and active['kind'] == 'shell' and kind == 'eof':
|
|
282
|
+
terminate()
|
|
283
|
+
send({'type': 'exit', 'status': 0, 'timed_out': False})
|
|
284
|
+
elif active and active['kind'] == 'shell' and kind == 'signal':
|
|
285
|
+
allowed = {'INT': signal.SIGINT, 'TERM': signal.SIGTERM, 'HUP': signal.SIGHUP,
|
|
286
|
+
'TSTP': signal.SIGTSTP, 'CONT': signal.SIGCONT}
|
|
287
|
+
sig = allowed.get(message.get('signal'))
|
|
288
|
+
group = os.tcgetpgrp(active['master'])
|
|
289
|
+
if sig is None or os.getsid(group) != active['process'].pid:
|
|
290
|
+
raise ValueError('Invalid shell foreground signal')
|
|
291
|
+
os.killpg(group, sig)
|
|
292
|
+
elif kind == 'upload' and active is None:
|
|
293
|
+
file = tempfile.NamedTemporaryFile(dir=ROOT, delete=False)
|
|
294
|
+
active = {'kind': 'upload', 'file': file, 'path': file.name, 'digest': hashlib.sha256(),
|
|
295
|
+
'deadline': time.monotonic() + 120}
|
|
296
|
+
send({'type': 'ready'})
|
|
297
|
+
elif kind == 'chunk' and active and active['kind'] == 'upload':
|
|
298
|
+
data = base64.b64decode(message['data'], validate=True)
|
|
299
|
+
if len(data) > CHUNK:
|
|
300
|
+
raise ValueError('Oversized upload chunk')
|
|
301
|
+
active['file'].write(data)
|
|
302
|
+
active['digest'].update(data)
|
|
303
|
+
active['deadline'] = time.monotonic() + 120
|
|
304
|
+
send({'type': 'ready'})
|
|
305
|
+
elif kind == 'install' and active and active['kind'] == 'upload':
|
|
306
|
+
destination, mode = message['destination'], message['mode']
|
|
307
|
+
if not isinstance(destination, str) or not destination.startswith('/') or '\0' in destination:
|
|
308
|
+
raise ValueError('Invalid destination')
|
|
309
|
+
if not isinstance(mode, str) or len(mode) != 4 or any(c not in '01234567' for c in mode):
|
|
310
|
+
raise ValueError('Invalid mode')
|
|
311
|
+
if active['digest'].hexdigest() != message['sha256']:
|
|
312
|
+
raise ValueError('Upload integrity mismatch')
|
|
313
|
+
active['file'].flush()
|
|
314
|
+
os.fsync(active['file'].fileno())
|
|
315
|
+
staged = active['path']
|
|
316
|
+
active['file'].close()
|
|
317
|
+
active = None
|
|
318
|
+
pending = None
|
|
319
|
+
try:
|
|
320
|
+
with tempfile.NamedTemporaryFile(dir=os.path.dirname(destination), delete=False) as target:
|
|
321
|
+
pending = target.name
|
|
322
|
+
with open(staged, 'rb') as source:
|
|
323
|
+
shutil.copyfileobj(source, target, CHUNK)
|
|
324
|
+
target.flush()
|
|
325
|
+
os.fchmod(target.fileno(), int(mode, 8))
|
|
326
|
+
os.fsync(target.fileno())
|
|
327
|
+
os.replace(pending, destination)
|
|
328
|
+
pending = None
|
|
329
|
+
send({'type': 'exit', 'status': 0, 'timed_out': False})
|
|
330
|
+
except OSError as error:
|
|
331
|
+
send({'type': 'stderr', 'data': base64.b64encode((os.strerror(error.errno) + '\n').encode()).decode()})
|
|
332
|
+
send({'type': 'exit', 'status': 1, 'timed_out': False})
|
|
333
|
+
finally:
|
|
334
|
+
os.unlink(staged)
|
|
335
|
+
if pending:
|
|
336
|
+
os.unlink(pending)
|
|
337
|
+
else:
|
|
338
|
+
raise ValueError('Unexpected protocol operation')
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def serial_input():
|
|
342
|
+
global buffer, request_id
|
|
343
|
+
while True:
|
|
344
|
+
try:
|
|
345
|
+
data = os.read(fd, CHUNK)
|
|
346
|
+
except BlockingIOError:
|
|
347
|
+
return
|
|
348
|
+
if not data:
|
|
349
|
+
terminate()
|
|
350
|
+
buffer.clear()
|
|
351
|
+
request_id = None
|
|
352
|
+
return
|
|
353
|
+
buffer.extend(data)
|
|
354
|
+
if len(buffer) > LIMIT:
|
|
355
|
+
raise ValueError('Oversized input frame')
|
|
356
|
+
while b'\n' in buffer:
|
|
357
|
+
line, _, remaining = buffer.partition(b'\n')
|
|
358
|
+
buffer = bytearray(remaining)
|
|
359
|
+
handle(json.loads(line))
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def output(pipe):
|
|
363
|
+
stream, kind = active['open'][pipe]
|
|
364
|
+
try:
|
|
365
|
+
data = os.read(pipe, CHUNK)
|
|
366
|
+
except BlockingIOError:
|
|
367
|
+
return
|
|
368
|
+
except OSError as error:
|
|
369
|
+
if error.errno != errno.EIO or active['kind'] != 'shell':
|
|
370
|
+
raise
|
|
371
|
+
data = b''
|
|
372
|
+
if data:
|
|
373
|
+
send({'type': kind, 'data': base64.b64encode(data).decode()})
|
|
374
|
+
else:
|
|
375
|
+
events.unregister(pipe)
|
|
376
|
+
del active['open'][pipe]
|
|
377
|
+
os.close(stream) if isinstance(stream, int) else stream.close()
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
try:
|
|
381
|
+
while True:
|
|
382
|
+
timeout = -1 if not active else 0.1
|
|
383
|
+
for device, flags in events.poll(timeout):
|
|
384
|
+
if device == fd:
|
|
385
|
+
serial_input()
|
|
386
|
+
elif active and active['kind'] in ('exec', 'shell') and device in active['open']:
|
|
387
|
+
if flags & (select.EPOLLIN | select.EPOLLHUP):
|
|
388
|
+
output(device)
|
|
389
|
+
if active and active['kind'] == 'shell' and flags & select.EPOLLOUT and device in active['open']:
|
|
390
|
+
flush_shell_input()
|
|
391
|
+
if active and active['deadline'] is not None and time.monotonic() >= active['deadline']:
|
|
392
|
+
terminate()
|
|
393
|
+
send({'type': 'exit', 'status': None, 'timed_out': True})
|
|
394
|
+
elif (active and active['kind'] in ('exec', 'shell') and
|
|
395
|
+
os.waitid(os.P_PID, active['process'].pid, os.WEXITED | os.WNOHANG | os.WNOWAIT)):
|
|
396
|
+
# wait() reaps only after every output pipe reaches EOF; no PID reuse while cancelling.
|
|
397
|
+
terminate_groups(active['process'].pid)
|
|
398
|
+
if active['open']:
|
|
399
|
+
continue
|
|
400
|
+
status = active['process'].wait()
|
|
401
|
+
active = None
|
|
402
|
+
send({'type': 'exit', 'status': status if status >= 0 else 128 - status, 'timed_out': False})
|
|
403
|
+
except (BrokenPipeError, EOFError, OSError) as error:
|
|
404
|
+
terminate()
|
|
405
|
+
print('Management transport failure: ' + type(error).__name__, file=sys.stderr)
|
|
406
|
+
raise SystemExit(1)
|
|
407
|
+
except Exception as error:
|
|
408
|
+
terminate()
|
|
409
|
+
# Never log command arguments, input frames, keys or uploaded content.
|
|
410
|
+
print('Management protocol failure: ' + type(error).__name__, file=sys.stderr)
|
|
411
|
+
raise SystemExit(1)
|
|
412
|
+
finally:
|
|
413
|
+
terminate()
|
|
414
|
+
events.close()
|
|
415
|
+
os.close(fd)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Seed management and the ordinary SSH account once, before project bootstrap.
|
|
3
|
+
set -eu
|
|
4
|
+
fail() { echo "VM management setup failed: $1; inspect node shell and serial logs" >&2; exit 1; }
|
|
5
|
+
interpreter=
|
|
6
|
+
for candidate in /usr/bin/python3 /usr/libexec/platform-python; do
|
|
7
|
+
if test -x "$candidate" && "$candidate" -c 'import sys; sys.exit(sys.version_info < (3, 5))'; then
|
|
8
|
+
interpreter="$candidate"
|
|
9
|
+
break
|
|
10
|
+
fi
|
|
11
|
+
done
|
|
12
|
+
test -n "$interpreter" || fail 'the selected cloud image requires Python 3 before bootstrap'
|
|
13
|
+
test ! -e /usr/local/libexec/empeira-management-python && test ! -L /usr/local/libexec/empeira-management-python || fail 'preexisting management interpreter'
|
|
14
|
+
ln -s "$interpreter" /usr/local/libexec/empeira-management-python
|
|
15
|
+
@SYSTEM_SETUP@
|
|
16
|
+
if test -e /sys/fs/selinux/enforce; then
|
|
17
|
+
restorecon -R /usr/local/libexec/empeira-management-agent /etc/empeira/management /var/lib/empeira
|
|
18
|
+
fi
|
|
19
|
+
systemctl daemon-reload
|
|
20
|
+
systemctl enable --now empeira-management.service
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
# Preserve interactive configuration and completion without persistent diagnostics history.
|
|
3
|
+
if [ -r /etc/bash.bashrc ]; then
|
|
4
|
+
. /etc/bash.bashrc
|
|
5
|
+
elif [ -r /etc/bashrc ]; then
|
|
6
|
+
. /etc/bashrc
|
|
7
|
+
fi
|
|
8
|
+
if [ -r "$HOME/.bashrc" ]; then
|
|
9
|
+
. "$HOME/.bashrc"
|
|
10
|
+
fi
|
|
11
|
+
HISTFILE=/dev/null
|
|
12
|
+
readonly HISTFILE
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Verify regular system SSH account metadata without exposing shadow contents or assigning a fixed UID/GID.
|
|
2
|
+
verify_account() {
|
|
3
|
+
empeira_account=$(getent passwd empeira) || fail 'missing system SSH account empeira'
|
|
4
|
+
empeira_uid=$(printf '%s\n' "$empeira_account" | cut -d : -f 3)
|
|
5
|
+
empeira_gid=$(printf '%s\n' "$empeira_account" | cut -d : -f 4)
|
|
6
|
+
empeira_home=$(printf '%s\n' "$empeira_account" | cut -d : -f 6)
|
|
7
|
+
empeira_shell=$(printf '%s\n' "$empeira_account" | cut -d : -f 7)
|
|
8
|
+
empeira_uid_min=$(awk '$1 == "UID_MIN" && $2 ~ /^[0-9]+$/ { print $2 }' /etc/login.defs)
|
|
9
|
+
for empeira_number in "$empeira_uid" "$empeira_gid" "$empeira_uid_min"; do
|
|
10
|
+
case "$empeira_number" in ''|*[!0-9]*) fail 'unverifiable system SSH UID/GID range' ;; esac
|
|
11
|
+
done
|
|
12
|
+
test "$empeira_uid" -gt 0 && test "$empeira_uid" -lt "$empeira_uid_min" || fail 'expected a dedicated system UID'
|
|
13
|
+
test "$empeira_gid" -gt 0 || fail 'system SSH account must not use the root group'
|
|
14
|
+
test "$empeira_home" = /var/lib/empeira || fail 'changed system SSH home; expected /var/lib/empeira'
|
|
15
|
+
test "$empeira_shell" = /bin/bash && test -x "$empeira_shell" || fail 'changed system SSH shell; expected /bin/bash'
|
|
16
|
+
empeira_shadow=$(getent shadow empeira) || fail 'cannot verify the system SSH password lock'
|
|
17
|
+
empeira_password=$(printf '%s\n' "$empeira_shadow" | cut -d : -f 2)
|
|
18
|
+
case "$empeira_password" in \!*|\**) ;; *) fail 'system SSH password must stay locked' ;; esac
|
|
19
|
+
unset empeira_shadow empeira_password empeira_account
|
|
20
|
+
test ! -L /var/lib/empeira || fail 'system SSH home is a symlink'
|
|
21
|
+
if test "$1" = creating && test ! -e /var/lib/empeira; then return; fi
|
|
22
|
+
test -d /var/lib/empeira || fail 'system SSH home is missing or not a directory'
|
|
23
|
+
test "$(stat -c '%u:%g:%a' /var/lib/empeira)" = "$empeira_uid:$empeira_gid:700" || fail 'system SSH home ownership or permissions changed'
|
|
24
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Cloud-init system users deliberately omit ssh_authorized_keys and home creation.
|
|
2
|
+
verify_account creating
|
|
3
|
+
if test ! -e /var/lib/empeira; then
|
|
4
|
+
install -d -m 0700 -o "$empeira_uid" -g "$empeira_gid" /var/lib/empeira
|
|
5
|
+
fi
|
|
6
|
+
verify_account verified
|
|
7
|
+
test ! -e /var/lib/empeira/.ssh && test ! -L /var/lib/empeira/.ssh || fail 'preexisting system SSH key directory'
|
|
8
|
+
install -d -m 0700 -o "$empeira_uid" -g "$empeira_gid" /var/lib/empeira/.ssh
|
|
9
|
+
install -m 0600 -o "$empeira_uid" -g "$empeira_gid" /etc/empeira/management/system_authorized_keys /var/lib/empeira/.ssh/authorized_keys
|
|
10
|
+
for empeira_profile in .bashrc .bash_profile .profile; do
|
|
11
|
+
if test -f "/etc/skel/$empeira_profile" && test ! -e "/var/lib/empeira/$empeira_profile"; then
|
|
12
|
+
test ! -L "/etc/skel/$empeira_profile" && test ! -L "/var/lib/empeira/$empeira_profile" || fail 'unsafe shell initialization path'
|
|
13
|
+
install -m 0600 -o "$empeira_uid" -g "$empeira_gid" "/etc/skel/$empeira_profile" "/var/lib/empeira/$empeira_profile"
|
|
14
|
+
fi
|
|
15
|
+
done
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
ARG EMPEIRA_BASE_NODE=@BASE_IMAGE@
|
|
2
|
+
FROM ${EMPEIRA_BASE_NODE}
|
|
3
|
+
RUN dnf install -y ca-certificates openssh-server systemd dbus iproute iputils bind-utils procps-ng sudo mlocate && dnf clean all
|
|
4
|
+
RUN command -v curl || dnf install -y curl
|
|
5
|
+
COPY node-start /usr/local/libexec/empeira-node-start
|
|
6
|
+
COPY node-connect /usr/local/libexec/empeira-ssh-connect.rb
|
|
7
|
+
COPY sshd_config /etc/ssh/sshd_config
|
|
8
|
+
RUN chmod 0755 /usr/local/libexec/empeira-node-start && rm -f /etc/ssh/ssh_host_* && systemctl enable sshd.service
|
|
9
|
+
ENTRYPOINT ["/usr/local/libexec/empeira-node-start"]
|
|
10
|
+
CMD ["/usr/sbin/sshd", "-D", "-e"]
|