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,273 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Linux endpoint for an authenticated runtime/SSH binary exec channel.
|
|
3
|
+
package main
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"crypto/sha256"
|
|
7
|
+
"fmt"
|
|
8
|
+
"io"
|
|
9
|
+
"net"
|
|
10
|
+
"os"
|
|
11
|
+
"os/signal"
|
|
12
|
+
"path/filepath"
|
|
13
|
+
"strconv"
|
|
14
|
+
"strings"
|
|
15
|
+
"syscall"
|
|
16
|
+
"time"
|
|
17
|
+
"unsafe"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
func ioctl(fd int, operation uintptr, value unsafe.Pointer) error {
|
|
21
|
+
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), operation, uintptr(value))
|
|
22
|
+
if errno != 0 {
|
|
23
|
+
return errno
|
|
24
|
+
}
|
|
25
|
+
return nil
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
func ifreq(name string) ([40]byte, error) {
|
|
29
|
+
var request [40]byte
|
|
30
|
+
if len(name) == 0 || len(name) > 15 {
|
|
31
|
+
return request, fmt.Errorf("invalid interface name")
|
|
32
|
+
}
|
|
33
|
+
copy(request[:16], name)
|
|
34
|
+
return request, nil
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func tap(name, bridge string) (*os.File, error) {
|
|
38
|
+
request, err := ifreq(name)
|
|
39
|
+
if err != nil {
|
|
40
|
+
return nil, err
|
|
41
|
+
}
|
|
42
|
+
f, err := os.OpenFile("/dev/net/tun", os.O_RDWR, 0)
|
|
43
|
+
if err != nil {
|
|
44
|
+
return nil, err
|
|
45
|
+
}
|
|
46
|
+
// TUN_EXCL prevents adopting an existing interface. No persistence: close removes it.
|
|
47
|
+
*(*uint16)(unsafe.Pointer(&request[16])) = 0x0002 | 0x1000 | 0x8000
|
|
48
|
+
if err = ioctl(int(f.Fd()), 0x400454ca, unsafe.Pointer(&request[0])); err != nil {
|
|
49
|
+
f.Close()
|
|
50
|
+
return nil, err
|
|
51
|
+
}
|
|
52
|
+
if err = attach(name, bridge); err != nil {
|
|
53
|
+
f.Close()
|
|
54
|
+
return nil, err
|
|
55
|
+
}
|
|
56
|
+
return f, nil
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
func attach(name, bridge string) error {
|
|
60
|
+
nic, err := net.InterfaceByName(name)
|
|
61
|
+
if err != nil {
|
|
62
|
+
return err
|
|
63
|
+
}
|
|
64
|
+
request, err := ifreq(bridge)
|
|
65
|
+
if err != nil {
|
|
66
|
+
return err
|
|
67
|
+
}
|
|
68
|
+
fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, 0)
|
|
69
|
+
if err != nil {
|
|
70
|
+
return err
|
|
71
|
+
}
|
|
72
|
+
defer syscall.Close(fd)
|
|
73
|
+
*(*int32)(unsafe.Pointer(&request[16])) = int32(nic.Index)
|
|
74
|
+
if err := ioctl(fd, 0x89a2, unsafe.Pointer(&request[0])); err != nil {
|
|
75
|
+
return err
|
|
76
|
+
} // SIOCBRADDIF
|
|
77
|
+
request, _ = ifreq(name)
|
|
78
|
+
*(*uint16)(unsafe.Pointer(&request[16])) = syscall.IFF_UP
|
|
79
|
+
return ioctl(fd, syscall.SIOCSIFFLAGS, unsafe.Pointer(&request[0]))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Docker's endpoint owns only its container network namespace. A local bridge
|
|
83
|
+
// joins its veth to the TAP; the TAP kernel path completes checksum/GSO work
|
|
84
|
+
// before frames enter the QEMU stream. No IP/application parsing is necessary.
|
|
85
|
+
func localBridge(name, ethernet string) error {
|
|
86
|
+
if _, err := net.InterfaceByName(name); err == nil {
|
|
87
|
+
master, err := os.Readlink("/sys/class/net/" + ethernet + "/master")
|
|
88
|
+
if err != nil || filepath.Base(master) != name {
|
|
89
|
+
return fmt.Errorf("unexpected existing adapter bridge")
|
|
90
|
+
}
|
|
91
|
+
return nil
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
req, err := ifreq(name)
|
|
95
|
+
if err != nil {
|
|
96
|
+
return err
|
|
97
|
+
}
|
|
98
|
+
fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, 0)
|
|
99
|
+
if err != nil {
|
|
100
|
+
return err
|
|
101
|
+
}
|
|
102
|
+
defer syscall.Close(fd)
|
|
103
|
+
if err = ioctl(fd, 0x89a0, unsafe.Pointer(&req[0])); err != nil {
|
|
104
|
+
return err
|
|
105
|
+
}
|
|
106
|
+
*(*uint16)(unsafe.Pointer(&req[16])) = syscall.IFF_UP
|
|
107
|
+
if err = ioctl(fd, syscall.SIOCSIFFLAGS, unsafe.Pointer(&req[0])); err != nil {
|
|
108
|
+
return err
|
|
109
|
+
}
|
|
110
|
+
if err = attach(ethernet, name); err != nil {
|
|
111
|
+
return err
|
|
112
|
+
}
|
|
113
|
+
if err = disableUplinkLearning(ethernet); err != nil {
|
|
114
|
+
return err
|
|
115
|
+
}
|
|
116
|
+
// Keep the runtime-assigned IPv4 as a local peer endpoint on the bridge.
|
|
117
|
+
// Forwarding stays disabled; SSH needs no host route or external publication.
|
|
118
|
+
address, err := ifreq(ethernet)
|
|
119
|
+
if err != nil {
|
|
120
|
+
return err
|
|
121
|
+
}
|
|
122
|
+
if err = ioctl(fd, syscall.SIOCGIFADDR, unsafe.Pointer(&address[0])); err != nil {
|
|
123
|
+
return err
|
|
124
|
+
}
|
|
125
|
+
mask, err := ifreq(ethernet)
|
|
126
|
+
if err != nil {
|
|
127
|
+
return err
|
|
128
|
+
}
|
|
129
|
+
if err = ioctl(fd, syscall.SIOCGIFNETMASK, unsafe.Pointer(&mask[0])); err != nil {
|
|
130
|
+
return err
|
|
131
|
+
}
|
|
132
|
+
req, err = ifreq(ethernet)
|
|
133
|
+
if err != nil {
|
|
134
|
+
return err
|
|
135
|
+
}
|
|
136
|
+
*(*uint16)(unsafe.Pointer(&req[16])) = syscall.AF_INET
|
|
137
|
+
if err = ioctl(fd, syscall.SIOCSIFADDR, unsafe.Pointer(&req[0])); err != nil {
|
|
138
|
+
return err
|
|
139
|
+
}
|
|
140
|
+
bridgeAddress, err := ifreq(name)
|
|
141
|
+
if err != nil {
|
|
142
|
+
return err
|
|
143
|
+
}
|
|
144
|
+
copy(bridgeAddress[16:32], address[16:32])
|
|
145
|
+
if err = ioctl(fd, syscall.SIOCSIFADDR, unsafe.Pointer(&bridgeAddress[0])); err != nil {
|
|
146
|
+
return err
|
|
147
|
+
}
|
|
148
|
+
copy(bridgeAddress[16:32], mask[16:32])
|
|
149
|
+
return ioctl(fd, syscall.SIOCSIFNETMASK, unsafe.Pointer(&bridgeAddress[0]))
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
func bridgeFrames(read func([]byte) (int, error), write func([]byte) error) error {
|
|
153
|
+
done := make(chan error, 2)
|
|
154
|
+
go func() {
|
|
155
|
+
for {
|
|
156
|
+
frame, err := readFrame(os.Stdin)
|
|
157
|
+
|
|
158
|
+
if err == nil {
|
|
159
|
+
err = write(frame)
|
|
160
|
+
}
|
|
161
|
+
if err != nil {
|
|
162
|
+
done <- err
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}()
|
|
167
|
+
go func() {
|
|
168
|
+
buf := make([]byte, maxFrame)
|
|
169
|
+
for {
|
|
170
|
+
n, err := read(buf)
|
|
171
|
+
|
|
172
|
+
if err == nil {
|
|
173
|
+
err = writeFrame(os.Stdout, buf[:n])
|
|
174
|
+
}
|
|
175
|
+
if err != nil {
|
|
176
|
+
done <- err
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}()
|
|
181
|
+
return <-done
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
func run() error {
|
|
185
|
+
if len(os.Args) == 5 && os.Args[1] == "connect" {
|
|
186
|
+
address := net.ParseIP(os.Args[2])
|
|
187
|
+
port, err := strconv.Atoi(os.Args[3])
|
|
188
|
+
source := net.ParseIP(os.Args[4])
|
|
189
|
+
if source == nil || source.To4() == nil || !source.IsPrivate() || address == nil || address.To4() == nil || !address.IsPrivate() || err != nil || port < 1 || port > 65535 {
|
|
190
|
+
return fmt.Errorf("invalid private SSH destination")
|
|
191
|
+
}
|
|
192
|
+
dialer := net.Dialer{Timeout: 5 * time.Second, LocalAddr: &net.TCPAddr{IP: source}}
|
|
193
|
+
connection, err := dialer.Dial("tcp4", net.JoinHostPort(address.String(), os.Args[3]))
|
|
194
|
+
if err != nil {
|
|
195
|
+
return err
|
|
196
|
+
}
|
|
197
|
+
defer connection.Close()
|
|
198
|
+
go func() {
|
|
199
|
+
_, err := io.Copy(connection, os.Stdin)
|
|
200
|
+
if err != nil {
|
|
201
|
+
connection.Close()
|
|
202
|
+
} else {
|
|
203
|
+
connection.(*net.TCPConn).CloseWrite()
|
|
204
|
+
}
|
|
205
|
+
}()
|
|
206
|
+
_, err = io.Copy(os.Stdout, connection)
|
|
207
|
+
return err
|
|
208
|
+
}
|
|
209
|
+
if len(os.Args) == 2 && os.Args[1] == "capabilities" {
|
|
210
|
+
data, err := os.ReadFile("/proc/self/status")
|
|
211
|
+
if err != nil {
|
|
212
|
+
return err
|
|
213
|
+
}
|
|
214
|
+
for _, line := range strings.Split(string(data), "\n") {
|
|
215
|
+
if strings.HasPrefix(line, "CapEff:") {
|
|
216
|
+
fmt.Println(strings.TrimSpace(strings.TrimPrefix(line, "CapEff:")))
|
|
217
|
+
return nil
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return fmt.Errorf("effective capabilities unavailable")
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if len(os.Args) == 2 && os.Args[1] == "hold" {
|
|
224
|
+
signals := make(chan os.Signal, 1)
|
|
225
|
+
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
|
|
226
|
+
<-signals
|
|
227
|
+
return nil
|
|
228
|
+
}
|
|
229
|
+
if len(os.Args) == 2 && os.Args[1] == "sha256" {
|
|
230
|
+
data, err := os.ReadFile("/proc/self/exe")
|
|
231
|
+
if err != nil {
|
|
232
|
+
return err
|
|
233
|
+
}
|
|
234
|
+
fmt.Printf("%x\n", sha256.Sum256(data))
|
|
235
|
+
return nil
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if len(os.Args) != 4 {
|
|
239
|
+
return fmt.Errorf("usage: adapter tap NAME BRIDGE | bridge NAME INTERFACE")
|
|
240
|
+
}
|
|
241
|
+
if os.Args[1] == "bridge" {
|
|
242
|
+
return localBridge(os.Args[2], os.Args[3])
|
|
243
|
+
}
|
|
244
|
+
if os.Args[1] != "tap" {
|
|
245
|
+
return fmt.Errorf("unsupported mode")
|
|
246
|
+
}
|
|
247
|
+
bridge := os.Args[3]
|
|
248
|
+
f, err := tap(os.Args[2], bridge)
|
|
249
|
+
if err != nil {
|
|
250
|
+
return err
|
|
251
|
+
}
|
|
252
|
+
defer f.Close()
|
|
253
|
+
fd := int(f.Fd())
|
|
254
|
+
if err = syscall.SetNonblock(fd, false); err != nil {
|
|
255
|
+
return err
|
|
256
|
+
}
|
|
257
|
+
// Opened before TUNSETIFF, the descriptor need not have joined Go's netpoller.
|
|
258
|
+
// Use blocking Linux I/O consistently; EOF on the exec channel ends the process.
|
|
259
|
+
return bridgeFrames(func(b []byte) (int, error) { return syscall.Read(fd, b) }, func(b []byte) error {
|
|
260
|
+
n, e := syscall.Write(fd, b)
|
|
261
|
+
if e == nil && n != len(b) {
|
|
262
|
+
e = io.ErrShortWrite
|
|
263
|
+
}
|
|
264
|
+
return e
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
func main() {
|
|
269
|
+
if err := run(); err != nil && err != io.EOF {
|
|
270
|
+
fmt.Fprintln(os.Stderr, err)
|
|
271
|
+
os.Exit(1)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-only
|
|
4
|
+
|
|
5
|
+
require_relative '../../lib/empeira'
|
|
6
|
+
|
|
7
|
+
path = Pathname(ARGV.fetch(0))
|
|
8
|
+
metadata = path.lstat
|
|
9
|
+
unless metadata.file? && metadata.uid == Process.uid && metadata.mode.nobits?(0o077)
|
|
10
|
+
abort 'Unsafe peer channel configuration'
|
|
11
|
+
end
|
|
12
|
+
Process.daemon(false, false)
|
|
13
|
+
begin
|
|
14
|
+
Empeira::Network::Peer::ChannelServer.new(path).run
|
|
15
|
+
rescue StandardError => e
|
|
16
|
+
File.open(path.dirname.join('channel.log'), 'a', 0o600) { |log| log.puts("#{e.class}: #{e.message}") }
|
|
17
|
+
exit 1
|
|
18
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-only
|
|
4
|
+
# SSH byte tunnel inside the rootless runtime namespace.
|
|
5
|
+
require 'socket'
|
|
6
|
+
require 'ipaddr'
|
|
7
|
+
|
|
8
|
+
address, value = ARGV.size == 1 ? ['127.0.0.1', ARGV.fetch(0)] : ARGV
|
|
9
|
+
ip = IPAddr.new(address)
|
|
10
|
+
raise ArgumentError, 'Invalid SSH destination' unless ip.ipv4? && (ip.private? || address == '127.0.0.1')
|
|
11
|
+
|
|
12
|
+
port = Integer(value, 10)
|
|
13
|
+
raise ArgumentError, 'Invalid SSH port' unless port.between?(1, 65_535)
|
|
14
|
+
|
|
15
|
+
begin
|
|
16
|
+
Socket.tcp(address, port, connect_timeout: 5) do |socket|
|
|
17
|
+
request = Thread.new do
|
|
18
|
+
IO.copy_stream($stdin, socket)
|
|
19
|
+
socket.close_write
|
|
20
|
+
rescue IOError, SystemCallError
|
|
21
|
+
nil
|
|
22
|
+
end
|
|
23
|
+
begin
|
|
24
|
+
IO.copy_stream(socket, $stdout)
|
|
25
|
+
ensure
|
|
26
|
+
request.kill.join
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
rescue IOError, SystemCallError => e
|
|
30
|
+
warn "System SSH connection failed: #{e.message}"
|
|
31
|
+
exit 1
|
|
32
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pathname'
|
|
4
|
+
require 'uri'
|
|
5
|
+
|
|
6
|
+
module Empeira
|
|
7
|
+
# Read-only bootstrap check. Repository management after bootstrap belongs to Puppet.
|
|
8
|
+
class AptSources
|
|
9
|
+
def initialize(root = '/')
|
|
10
|
+
@root = Pathname(root)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def valid?
|
|
14
|
+
release = @root.join('etc/os-release').read
|
|
15
|
+
return true unless release.match?(/^ID=["']?(?:ubuntu|debian)["']?$/)
|
|
16
|
+
|
|
17
|
+
apt = @root.join('etc/apt')
|
|
18
|
+
lists = [apt.join('sources.list'), *apt.glob('sources.list.d/*.list')]
|
|
19
|
+
lists.any? { |path| path.file? && legacy?(path.read) } ||
|
|
20
|
+
apt.glob('sources.list.d/*.sources').any? { |path| deb822?(path.read) }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def uri?(value)
|
|
26
|
+
uri = URI.parse(value)
|
|
27
|
+
%w[http https file].include?(uri.scheme) && (uri.scheme == 'file' || !uri.host.to_s.empty?)
|
|
28
|
+
rescue URI::InvalidURIError
|
|
29
|
+
false
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def legacy?(content)
|
|
33
|
+
content.lines.any? do |line|
|
|
34
|
+
fields = line.sub(/#.*/, '').strip.sub(/\Adeb\s+\[[^\]]*\]\s+/, 'deb ').split
|
|
35
|
+
fields.first == 'deb' && fields.length >= 4 && uri?(fields[1]) && fields.drop(3).include?('main')
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def deb822?(content)
|
|
40
|
+
content.gsub(/^#.*\n?/, '').gsub(/\n[ \t]+/, ' ').split(/\n\s*\n/).any? do |paragraph|
|
|
41
|
+
active?(fields(paragraph))
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def fields(paragraph)
|
|
46
|
+
paragraph.lines.filter_map do |line|
|
|
47
|
+
key, value = line.split(':', 2)
|
|
48
|
+
[key.downcase, value.strip] if value
|
|
49
|
+
end.to_h
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def active?(fields)
|
|
53
|
+
fields.fetch('enabled', 'yes').downcase != 'no' && fields.fetch('types', '').split.include?('deb') &&
|
|
54
|
+
fields.fetch('uris', '').split.any? { |value| uri?(value) } &&
|
|
55
|
+
!fields.fetch('suites', '').empty? && fields.fetch('components', '').split.include?('main')
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
if $PROGRAM_NAME == __FILE__
|
|
61
|
+
begin
|
|
62
|
+
exit(Empeira::AptSources.new.valid? ? 0 : 1)
|
|
63
|
+
rescue StandardError
|
|
64
|
+
exit 1
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
ARG EMPEIRA_BASE_NODE=@BASE_IMAGE@
|
|
2
|
+
FROM ${EMPEIRA_BASE_NODE}
|
|
3
|
+
ENV DEBIAN_FRONTEND=noninteractive
|
|
4
|
+
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates openssh-server curl systemd systemd-sysv dbus iproute2 iputils-ping dnsutils procps sudo plocate
|
|
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 ssh.service
|
|
9
|
+
ENTRYPOINT ["/usr/local/libexec/empeira-node-start"]
|
|
10
|
+
CMD ["/usr/sbin/sshd", "-D", "-e"]
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-only
|
|
4
|
+
# Guest-only supervisor, invoked by an owned native container exec as numeric UID 0.
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'timeout'
|
|
7
|
+
|
|
8
|
+
module EmpeiraDiagnosticExec
|
|
9
|
+
ROOT = '/run/empeira-exec'
|
|
10
|
+
|
|
11
|
+
# rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity -- Both root and session directory must remain private numeric-root paths.
|
|
12
|
+
def self.directory(token, create: false)
|
|
13
|
+
raise 'Invalid diagnostic identity' unless token.match?(/\A[0-9a-f]{32}\z/)
|
|
14
|
+
|
|
15
|
+
path = File.join(ROOT, token)
|
|
16
|
+
[ROOT, path].each do |entry|
|
|
17
|
+
if create
|
|
18
|
+
begin
|
|
19
|
+
Dir.mkdir(entry, 0o700)
|
|
20
|
+
rescue Errno::EEXIST
|
|
21
|
+
raise 'Existing diagnostic session' if entry == path
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
metadata = File.lstat(entry)
|
|
25
|
+
unless metadata.directory? && metadata.uid.zero? && metadata.mode.nobits?(0o077)
|
|
26
|
+
raise 'Unsafe diagnostic directory'
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
path
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity -- Scan only the pinned owned process session before TERM/KILL.
|
|
33
|
+
def self.cleanup_groups
|
|
34
|
+
groups = Dir.glob('/proc/[0-9]*/stat').filter_map do |path|
|
|
35
|
+
fields = File.read(path).rpartition(') ').last.split
|
|
36
|
+
Integer(fields[2]) if Integer(fields[3]) == Process.pid && Integer(fields[2]) != Process.getpgrp
|
|
37
|
+
rescue Errno::ENOENT, Errno::ESRCH
|
|
38
|
+
nil
|
|
39
|
+
end.uniq
|
|
40
|
+
%w[TERM KILL].each do |signal|
|
|
41
|
+
groups.each do |group|
|
|
42
|
+
Process.kill(signal, -group)
|
|
43
|
+
rescue Errno::ESRCH
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
sleep 0.1 if signal == 'TERM' && !groups.empty?
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity -- Completion is published only after interruption, timeout and owned-group cleanup.
|
|
51
|
+
def self.run(token, duration, arguments)
|
|
52
|
+
raise 'Diagnostic supervisor requires UID 0' unless Process.uid.zero?
|
|
53
|
+
|
|
54
|
+
path = directory(token, create: true)
|
|
55
|
+
begin
|
|
56
|
+
Process.setsid
|
|
57
|
+
rescue Errno::EPERM
|
|
58
|
+
raise unless Process.getsid == Process.pid
|
|
59
|
+
end
|
|
60
|
+
owner = Thread.current
|
|
61
|
+
monitor = Thread.new do
|
|
62
|
+
$stdin.read(1)
|
|
63
|
+
owner.raise Interrupt
|
|
64
|
+
end
|
|
65
|
+
environment = { 'PATH' => '/opt/puppetlabs/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' }
|
|
66
|
+
pid = Process.spawn(environment, [arguments.first, arguments.first], *arguments.drop(1),
|
|
67
|
+
in: File::NULL, pgroup: true)
|
|
68
|
+
status = Timeout.timeout(duration == 'none' ? nil : Float(duration)) { Process.wait2(pid).last }
|
|
69
|
+
result = { 'status' => status.exitstatus || (128 + status.termsig), 'timed_out' => false }
|
|
70
|
+
rescue Interrupt
|
|
71
|
+
result = { 'status' => 130, 'timed_out' => false }
|
|
72
|
+
rescue Timeout::Error
|
|
73
|
+
result = { 'status' => nil, 'timed_out' => true }
|
|
74
|
+
rescue SystemCallError => e
|
|
75
|
+
warn "Guest command could not start (#{e.class.name})"
|
|
76
|
+
result = { 'status' => e.is_a?(Errno::ENOENT) ? 127 : 126, 'timed_out' => false }
|
|
77
|
+
ensure
|
|
78
|
+
monitor&.kill&.join
|
|
79
|
+
cleanup_groups if path && Process.getsid == Process.pid
|
|
80
|
+
begin
|
|
81
|
+
Process.wait(pid) if pid
|
|
82
|
+
rescue Errno::ECHILD
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
if result && path
|
|
86
|
+
File.open(File.join(path, 'status'), File::WRONLY | File::CREAT | File::EXCL, 0o600) do |file|
|
|
87
|
+
file.write(JSON.generate(result))
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def self.completion(token)
|
|
93
|
+
path = directory(token)
|
|
94
|
+
file = File.join(path, 'status')
|
|
95
|
+
metadata = File.lstat(file)
|
|
96
|
+
raise 'Unsafe diagnostic completion' unless metadata.file? && metadata.uid.zero? && metadata.mode.nobits?(0o077)
|
|
97
|
+
|
|
98
|
+
print File.read(file, 1024)
|
|
99
|
+
File.unlink(file)
|
|
100
|
+
Dir.rmdir(path)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
mode, token, duration, *arguments = ARGV
|
|
105
|
+
if mode == 'read'
|
|
106
|
+
EmpeiraDiagnosticExec.completion(token)
|
|
107
|
+
else
|
|
108
|
+
EmpeiraDiagnosticExec.run(token, duration, arguments)
|
|
109
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'pathname'
|
|
5
|
+
require 'rbconfig'
|
|
6
|
+
require 'shellwords'
|
|
7
|
+
require 'tempfile'
|
|
8
|
+
require 'fileutils'
|
|
9
|
+
|
|
10
|
+
module Empeira
|
|
11
|
+
class InteractiveToolsGuest
|
|
12
|
+
PROFILE = '/etc/profile.d/90-empeira-tools.sh'
|
|
13
|
+
BEGIN_MARKER = "# BEGIN EMPEIRA INTERACTIVE TOOLS\n"
|
|
14
|
+
END_MARKER = "# END EMPEIRA INTERACTIVE TOOLS\n"
|
|
15
|
+
|
|
16
|
+
def initialize(request, root: Pathname('/'))
|
|
17
|
+
@root = Pathname(root)
|
|
18
|
+
@candidates = [Pathname(request.fetch('puppet')).dirname.to_s, RbConfig::CONFIG.fetch('bindir')].uniq
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def reconcile
|
|
22
|
+
directory = tool_directory
|
|
23
|
+
changed = profile(directory)
|
|
24
|
+
global_initializers.each { |target| changed = bash(target) || changed }
|
|
25
|
+
{ 'changed' => changed, 'directory' => directory }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def tool_directory
|
|
31
|
+
directory = @candidates.find do |candidate|
|
|
32
|
+
%w[puppet facter].all? { |command| path(candidate).join(command).executable? }
|
|
33
|
+
end
|
|
34
|
+
directory || raise('Cannot locate installed puppet and facter tools together')
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def global_initializers
|
|
38
|
+
initializers = %w[/etc/bash.bashrc /etc/bashrc].map { |value| path(value) }.select(&:exist?)
|
|
39
|
+
raise 'No supported global Bash initializer; inspect the image shell setup' if initializers.empty?
|
|
40
|
+
|
|
41
|
+
initializers
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def path(value)
|
|
45
|
+
@root.join(value.delete_prefix('/'))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def read(target)
|
|
49
|
+
target.parent.ascend do |parent|
|
|
50
|
+
raise 'Unsafe interactive startup directory' if parent.symlink?
|
|
51
|
+
end
|
|
52
|
+
stat = target.lstat
|
|
53
|
+
unless stat.file? && stat.nlink == 1 && stat.uid == Process.euid && stat.mode.nobits?(0o022)
|
|
54
|
+
raise 'Unsafe or unowned interactive startup file'
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
target.binread
|
|
58
|
+
rescue Errno::ENOENT
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def write(target, content, mode: nil)
|
|
63
|
+
FileUtils.mkdir_p(target.parent, mode: 0o755)
|
|
64
|
+
mode ||= target.exist? ? target.stat.mode & 0o777 : 0o644
|
|
65
|
+
Tempfile.create('.empeira-tools-', target.parent) do |file|
|
|
66
|
+
file.chmod(mode)
|
|
67
|
+
file.write(content)
|
|
68
|
+
file.flush
|
|
69
|
+
file.fsync
|
|
70
|
+
File.rename(file.path, target)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def profile_content(directory)
|
|
75
|
+
<<~SH
|
|
76
|
+
# Managed by Empeira: installed agent tools, interactive shells only.
|
|
77
|
+
case $- in
|
|
78
|
+
*i*)
|
|
79
|
+
empeira_tool_bin=#{Shellwords.escape(path(directory).to_s)}
|
|
80
|
+
case ":${PATH-}:" in
|
|
81
|
+
*":${empeira_tool_bin}:"*) ;;
|
|
82
|
+
*) PATH="${empeira_tool_bin}${PATH:+:$PATH}"; export PATH ;;
|
|
83
|
+
esac
|
|
84
|
+
unset empeira_tool_bin
|
|
85
|
+
;;
|
|
86
|
+
esac
|
|
87
|
+
SH
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# rubocop:disable-next Naming/PredicateMethod -- Reconciliation writes an owned profile fragment.
|
|
91
|
+
def profile(directory)
|
|
92
|
+
target = path(PROFILE)
|
|
93
|
+
current = read(target)
|
|
94
|
+
if current && @candidates.none? { |candidate| current == profile_content(candidate) }
|
|
95
|
+
raise 'Foreign or modified /etc/profile.d/90-empeira-tools.sh; refusing to overwrite it'
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
content = profile_content(directory)
|
|
99
|
+
return false if current == content && (target.stat.mode & 0o777) == 0o644
|
|
100
|
+
|
|
101
|
+
write(target, content, mode: 0o644)
|
|
102
|
+
true
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def hook
|
|
106
|
+
<<~SH
|
|
107
|
+
#{BEGIN_MARKER.chomp}
|
|
108
|
+
case $- in
|
|
109
|
+
*i*) . #{Shellwords.escape(path(PROFILE).to_s)} ;;
|
|
110
|
+
esac
|
|
111
|
+
#{END_MARKER.chomp}
|
|
112
|
+
SH
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# rubocop:disable-next Naming/PredicateMethod -- Preserve every foreign line in the global Bash initializer.
|
|
116
|
+
def bash(target)
|
|
117
|
+
current = read(target)
|
|
118
|
+
if current.include?(BEGIN_MARKER) || current.include?(END_MARKER)
|
|
119
|
+
unless current.scan(BEGIN_MARKER).size == 1 && current.scan(END_MARKER).size == 1 && current.include?(hook)
|
|
120
|
+
raise 'Modified Empeira global Bash hook; refusing to overwrite it'
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
return false
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
write(target, "#{hook}#{current}")
|
|
127
|
+
true
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
if $PROGRAM_NAME == __FILE__
|
|
133
|
+
begin
|
|
134
|
+
puts JSON.generate(Empeira::InteractiveToolsGuest.new(JSON.parse(ARGV.fetch(0))).reconcile)
|
|
135
|
+
rescue StandardError => e
|
|
136
|
+
warn e.message
|
|
137
|
+
exit 1
|
|
138
|
+
end
|
|
139
|
+
end
|