mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-09 16:00:59 +00:00
Wire legacy sensor-proxy cleanup into install.sh --uninstall (#34)
A Proxmox host upgraded from v5 may still carry the legacy pulse-sensor-proxy footprint (binary, systemd units, runtime/state dirs, dedicated service user, and managed SSH keys in root's authorized_keys). install.sh --uninstall removed everything for the Pulse server itself but left that legacy footprint behind, so a 'complete uninstall' was not complete -- most notably it left SSH key entries in /root/.ssh/authorized_keys. uninstall_pulse now calls cleanup_local_sensor_proxy, which removes the LOCAL footprint only: stop/disable the units, remove the binary/units/runtime/state dirs, strip the '# pulse-managed-key' / '# pulse-proxy-key' entries, and remove the service user/group. It is presence-gated (silent no-op when no proxy was installed). The aggressive cluster-wide authorized_keys removal and pulse-monitor@pam API-user deletion stay behind the explicit standalone scripts/uninstall-sensor-proxy.sh, which we print a pointer to. Functional + contract tests in scripts/installtests/root_install_sh_test.go; deployment-installability contract documents the new uninstall removal scope.
This commit is contained in:
parent
1a42011724
commit
2ff2b18c74
3 changed files with 254 additions and 1 deletions
|
|
@ -1114,6 +1114,18 @@ installs too: the root `install.sh`, generated update helper, and
|
|||
identity across install, update, reset, uninstall, and timer/service wiring so
|
||||
stable and preview Pulse runtimes can coexist on one host without drifting back
|
||||
onto the default `pulse.service` paths.
|
||||
That same server-installer uninstall must also leave no legacy companion
|
||||
footprint behind on the host: `install.sh --uninstall` removes the local
|
||||
`pulse-sensor-proxy` artifacts a v5-era Proxmox host may still carry — the
|
||||
binary, its systemd units, runtime/state directories, the dedicated service
|
||||
user/group, and the managed `# pulse-managed-key` / `# pulse-proxy-key` entries
|
||||
in root's `authorized_keys` — through one installer-owned
|
||||
`cleanup_local_sensor_proxy` helper that is presence-gated (a silent no-op when
|
||||
no proxy was installed). The aggressive cluster-wide authorized_keys removal and
|
||||
`pulse-monitor@pam` API-user deletion stay behind the explicit standalone
|
||||
`scripts/uninstall-sensor-proxy.sh`, which the installer only prints a pointer
|
||||
to. `scripts/installtests/root_install_sh_test.go` is the owned proof surface
|
||||
for that local sensor-proxy cleanup.
|
||||
That same server-installer boundary also owns release trust fail-closed: the
|
||||
root `install.sh`, its generated update helper, and
|
||||
`scripts/pulse-auto-update.sh` must verify downloaded release tarballs and
|
||||
|
|
|
|||
93
install.sh
93
install.sh
|
|
@ -4737,6 +4737,93 @@ main() {
|
|||
fi
|
||||
}
|
||||
|
||||
# Legacy pulse-sensor-proxy footprint (v4/v5). install.sh never installs the
|
||||
# sensor-proxy in v6 — the unified agent replaced it — but a Proxmox host that
|
||||
# was upgraded from v5 may still carry it. These paths mirror the LOCAL subset
|
||||
# of scripts/uninstall-sensor-proxy.sh so a full `--uninstall` on such a host
|
||||
# leaves nothing behind: binary, units, runtime/state, service user, and —
|
||||
# security-relevant — the managed SSH keys in root's authorized_keys. The
|
||||
# cluster-wide key removal and Proxmox API-user deletion stay behind the
|
||||
# explicit standalone script (we print a pointer to it).
|
||||
SENSOR_PROXY_BINARY_PATH="${PULSE_SENSOR_PROXY_BINARY_PATH:-/usr/local/bin/pulse-sensor-proxy}"
|
||||
SENSOR_PROXY_INSTALL_ROOT="${PULSE_SENSOR_PROXY_INSTALL_ROOT:-/opt/pulse/sensor-proxy}"
|
||||
SENSOR_PROXY_RUNTIME_DIR="${PULSE_SENSOR_PROXY_RUNTIME_DIR:-/run/pulse-sensor-proxy}"
|
||||
SENSOR_PROXY_WORK_DIR="${PULSE_SENSOR_PROXY_WORK_DIR:-/var/lib/pulse-sensor-proxy}"
|
||||
SENSOR_PROXY_CONFIG_DIR="${PULSE_SENSOR_PROXY_CONFIG_DIR:-/etc/pulse-sensor-proxy}"
|
||||
SENSOR_PROXY_LOG_DIR="${PULSE_SENSOR_PROXY_LOG_DIR:-/var/log/pulse/sensor-proxy}"
|
||||
SENSOR_PROXY_SERVICE_USER="${PULSE_SENSOR_PROXY_SERVICE_USER:-pulse-sensor-proxy}"
|
||||
SENSOR_PROXY_AUTHORIZED_KEYS_PATH="${PULSE_SENSOR_PROXY_AUTHORIZED_KEYS_PATH:-/root/.ssh/authorized_keys}"
|
||||
SENSOR_PROXY_SYSTEMD_DIR="${PULSE_SENSOR_PROXY_SYSTEMD_DIR:-/etc/systemd/system}"
|
||||
|
||||
local_sensor_proxy_present() {
|
||||
[[ -e "$SENSOR_PROXY_BINARY_PATH" ]] && return 0
|
||||
[[ -e "${SENSOR_PROXY_SYSTEMD_DIR}/pulse-sensor-proxy.service" ]] && return 0
|
||||
[[ -d "$SENSOR_PROXY_INSTALL_ROOT" ]] && return 0
|
||||
[[ -d "$SENSOR_PROXY_WORK_DIR" ]] && return 0
|
||||
[[ -d "$SENSOR_PROXY_CONFIG_DIR" ]] && return 0
|
||||
return 1
|
||||
}
|
||||
|
||||
remove_local_sensor_proxy_managed_keys() {
|
||||
local auth_file="$SENSOR_PROXY_AUTHORIZED_KEYS_PATH"
|
||||
local tmp_file=""
|
||||
[[ -f "$auth_file" ]] || return 0
|
||||
tmp_file=$(mktemp) || return 0
|
||||
grep -v -E '# pulse-(managed|proxy)-key$' "$auth_file" >"$tmp_file" 2>/dev/null || true
|
||||
if cmp -s "$tmp_file" "$auth_file"; then
|
||||
rm -f "$tmp_file"
|
||||
return 0
|
||||
fi
|
||||
chmod --reference="$auth_file" "$tmp_file" 2>/dev/null || chmod 600 "$tmp_file" 2>/dev/null || true
|
||||
chown --reference="$auth_file" "$tmp_file" 2>/dev/null || true
|
||||
mv "$tmp_file" "$auth_file"
|
||||
echo "Removed legacy pulse-sensor-proxy SSH key entries from $auth_file"
|
||||
}
|
||||
|
||||
cleanup_local_sensor_proxy() {
|
||||
local_sensor_proxy_present || return 0
|
||||
|
||||
echo "Removing legacy pulse-sensor-proxy footprint from this host..."
|
||||
|
||||
local unit=""
|
||||
for unit in \
|
||||
pulse-sensor-proxy.service \
|
||||
pulse-sensor-proxy-selfheal.timer \
|
||||
pulse-sensor-proxy-selfheal.service \
|
||||
pulse-sensor-cleanup.path \
|
||||
pulse-sensor-cleanup.service; do
|
||||
systemctl stop "$unit" >/dev/null 2>&1 || true
|
||||
systemctl disable "$unit" >/dev/null 2>&1 || true
|
||||
done
|
||||
|
||||
rm -f "$SENSOR_PROXY_BINARY_PATH"
|
||||
rm -f /usr/local/bin/pulse-sensor-cleanup.sh
|
||||
rm -f "${SENSOR_PROXY_SYSTEMD_DIR}/pulse-sensor-proxy.service"
|
||||
rm -f "${SENSOR_PROXY_SYSTEMD_DIR}/pulse-sensor-proxy-selfheal.service"
|
||||
rm -f "${SENSOR_PROXY_SYSTEMD_DIR}/pulse-sensor-proxy-selfheal.timer"
|
||||
rm -f "${SENSOR_PROXY_SYSTEMD_DIR}/pulse-sensor-cleanup.service"
|
||||
rm -f "${SENSOR_PROXY_SYSTEMD_DIR}/pulse-sensor-cleanup.path"
|
||||
rm -rf "$SENSOR_PROXY_RUNTIME_DIR"
|
||||
rm -rf "$SENSOR_PROXY_INSTALL_ROOT"
|
||||
rm -rf "$SENSOR_PROXY_WORK_DIR"
|
||||
rm -rf "$SENSOR_PROXY_CONFIG_DIR"
|
||||
rm -rf "$SENSOR_PROXY_LOG_DIR"
|
||||
|
||||
remove_local_sensor_proxy_managed_keys
|
||||
|
||||
if id -u "$SENSOR_PROXY_SERVICE_USER" >/dev/null 2>&1; then
|
||||
userdel --remove "$SENSOR_PROXY_SERVICE_USER" >/dev/null 2>&1 || userdel "$SENSOR_PROXY_SERVICE_USER" >/dev/null 2>&1 || true
|
||||
fi
|
||||
if getent group "$SENSOR_PROXY_SERVICE_USER" >/dev/null 2>&1; then
|
||||
groupdel "$SENSOR_PROXY_SERVICE_USER" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
echo "Removed legacy pulse-sensor-proxy footprint from this host."
|
||||
echo "If this host belongs to a Proxmox cluster, run the full cleanup on the"
|
||||
echo "other nodes (and to remove the pulse-monitor@pam API user):"
|
||||
echo " curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/main/scripts/uninstall-sensor-proxy.sh | bash -s -- --purge --remove-proxmox-access"
|
||||
}
|
||||
|
||||
# Uninstall function
|
||||
uninstall_pulse() {
|
||||
check_root
|
||||
|
|
@ -4787,7 +4874,11 @@ uninstall_pulse() {
|
|||
echo "Removing pulse user..."
|
||||
userdel pulse 2>/dev/null || true
|
||||
fi
|
||||
|
||||
|
||||
# Remove any leftover legacy pulse-sensor-proxy footprint on this host so a
|
||||
# full uninstall on a v5-upgraded Proxmox host leaves nothing behind.
|
||||
cleanup_local_sensor_proxy
|
||||
|
||||
# Reload systemd
|
||||
systemctl daemon-reload
|
||||
|
||||
|
|
|
|||
|
|
@ -573,6 +573,156 @@ func TestOperatorInstallDocsAvoidUnverifiedBootstrapAndFloatingImageTags(t *test
|
|||
// proxies the top-level GitHub install.sh asset, which is the SERVER installer
|
||||
// (not the agent installer) and rejects the wizard's --url/--token-file flags.
|
||||
// The Docker image deploys these sidecars; deploy_agent_scripts must too.
|
||||
// TestRootInstallUninstallCleansLegacySensorProxy guards #34: `install.sh
|
||||
// --uninstall` on a Proxmox host that was upgraded from v5 must remove the
|
||||
// leftover pulse-sensor-proxy footprint locally — binary, units, runtime/state,
|
||||
// service user, and (security-relevant) the managed SSH keys in root's
|
||||
// authorized_keys — so a "complete uninstall" leaves nothing behind. Cluster-
|
||||
// wide key removal and Proxmox API-user deletion stay behind the explicit
|
||||
// standalone scripts/uninstall-sensor-proxy.sh, which we only point users to.
|
||||
func TestRootInstallUninstallCleansLegacySensorProxy(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
|
||||
binPath := filepath.Join(tmp, "bin", "pulse-sensor-proxy")
|
||||
systemdDir := filepath.Join(tmp, "systemd")
|
||||
unitPath := filepath.Join(systemdDir, "pulse-sensor-proxy.service")
|
||||
authKeys := filepath.Join(tmp, "authorized_keys")
|
||||
marker := filepath.Join(tmp, "calls.log")
|
||||
|
||||
for _, dir := range []string{filepath.Dir(binPath), systemdDir} {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
t.Fatalf("mkdir %s: %v", dir, err)
|
||||
}
|
||||
}
|
||||
if err := os.WriteFile(binPath, []byte("#!/bin/sh\n"), 0o755); err != nil {
|
||||
t.Fatalf("write binary: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(unitPath, []byte("[Unit]\n"), 0o644); err != nil {
|
||||
t.Fatalf("write unit: %v", err)
|
||||
}
|
||||
authContent := "ssh-ed25519 AAAA keepme@admin\n" +
|
||||
"ssh-ed25519 BBBB # pulse-managed-key\n" +
|
||||
"ssh-ed25519 CCCC # pulse-proxy-key\n" +
|
||||
"ssh-rsa DDDD keep-this-too\n"
|
||||
if err := os.WriteFile(authKeys, []byte(authContent), 0o600); err != nil {
|
||||
t.Fatalf("write authorized_keys: %v", err)
|
||||
}
|
||||
|
||||
env := `
|
||||
set -uo pipefail
|
||||
SENSOR_PROXY_BINARY_PATH="` + binPath + `"
|
||||
SENSOR_PROXY_SYSTEMD_DIR="` + systemdDir + `"
|
||||
SENSOR_PROXY_INSTALL_ROOT="` + filepath.Join(tmp, "sensor-proxy") + `"
|
||||
SENSOR_PROXY_RUNTIME_DIR="` + filepath.Join(tmp, "run") + `"
|
||||
SENSOR_PROXY_WORK_DIR="` + filepath.Join(tmp, "work") + `"
|
||||
SENSOR_PROXY_CONFIG_DIR="` + filepath.Join(tmp, "config") + `"
|
||||
SENSOR_PROXY_LOG_DIR="` + filepath.Join(tmp, "log") + `"
|
||||
SENSOR_PROXY_SERVICE_USER="pulse-sensor-proxy-test"
|
||||
SENSOR_PROXY_AUTHORIZED_KEYS_PATH="` + authKeys + `"
|
||||
systemctl() { return 0; }
|
||||
userdel() { echo "userdel $*" >>"` + marker + `"; return 0; }
|
||||
groupdel() { echo "groupdel $*" >>"` + marker + `"; return 0; }
|
||||
id() { return 0; }
|
||||
getent() { return 0; }
|
||||
`
|
||||
|
||||
funcs := extractRootInstallShellFunction(t, "local_sensor_proxy_present") + "\n" +
|
||||
extractRootInstallShellFunction(t, "remove_local_sensor_proxy_managed_keys") + "\n" +
|
||||
extractRootInstallShellFunction(t, "cleanup_local_sensor_proxy")
|
||||
|
||||
out, err := exec.Command("bash", "-c", env+funcs+"\ncleanup_local_sensor_proxy\n").CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("cleanup_local_sensor_proxy failed: %v\n%s", err, out)
|
||||
}
|
||||
|
||||
if _, statErr := os.Stat(binPath); !os.IsNotExist(statErr) {
|
||||
t.Fatalf("expected sensor-proxy binary removed, stat err = %v", statErr)
|
||||
}
|
||||
if _, statErr := os.Stat(unitPath); !os.IsNotExist(statErr) {
|
||||
t.Fatalf("expected sensor-proxy unit removed, stat err = %v", statErr)
|
||||
}
|
||||
|
||||
keysAfter, err := os.ReadFile(authKeys)
|
||||
if err != nil {
|
||||
t.Fatalf("read authorized_keys after cleanup: %v", err)
|
||||
}
|
||||
keysText := string(keysAfter)
|
||||
if strings.Contains(keysText, "pulse-managed-key") || strings.Contains(keysText, "pulse-proxy-key") {
|
||||
t.Fatalf("expected managed/proxy SSH keys stripped, got:\n%s", keysText)
|
||||
}
|
||||
for _, keep := range []string{"keepme@admin", "keep-this-too"} {
|
||||
if !strings.Contains(keysText, keep) {
|
||||
t.Fatalf("expected unrelated SSH key %q preserved, got:\n%s", keep, keysText)
|
||||
}
|
||||
}
|
||||
|
||||
markerBytes, err := os.ReadFile(marker)
|
||||
if err != nil {
|
||||
t.Fatalf("expected userdel/groupdel to run: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(markerBytes), "userdel") {
|
||||
t.Fatalf("expected service user removal, got marker:\n%s", markerBytes)
|
||||
}
|
||||
|
||||
if !strings.Contains(string(out), "uninstall-sensor-proxy.sh") {
|
||||
t.Fatalf("expected pointer to standalone cluster cleanup script, got:\n%s", out)
|
||||
}
|
||||
|
||||
// Presence-gated: a host with no sensor-proxy footprint is a silent no-op.
|
||||
empty := t.TempDir()
|
||||
noopEnv := `
|
||||
set -uo pipefail
|
||||
SENSOR_PROXY_BINARY_PATH="` + filepath.Join(empty, "pulse-sensor-proxy") + `"
|
||||
SENSOR_PROXY_SYSTEMD_DIR="` + empty + `"
|
||||
SENSOR_PROXY_INSTALL_ROOT="` + filepath.Join(empty, "sensor-proxy") + `"
|
||||
SENSOR_PROXY_RUNTIME_DIR="` + filepath.Join(empty, "run") + `"
|
||||
SENSOR_PROXY_WORK_DIR="` + filepath.Join(empty, "work") + `"
|
||||
SENSOR_PROXY_CONFIG_DIR="` + filepath.Join(empty, "config") + `"
|
||||
SENSOR_PROXY_LOG_DIR="` + filepath.Join(empty, "log") + `"
|
||||
SENSOR_PROXY_SERVICE_USER="pulse-sensor-proxy-test"
|
||||
SENSOR_PROXY_AUTHORIZED_KEYS_PATH="` + filepath.Join(empty, "authorized_keys") + `"
|
||||
systemctl() { return 0; }
|
||||
userdel() { return 0; }
|
||||
groupdel() { return 0; }
|
||||
id() { return 0; }
|
||||
getent() { return 0; }
|
||||
`
|
||||
noopOut, err := exec.Command("bash", "-c", noopEnv+funcs+"\ncleanup_local_sensor_proxy\n").CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("cleanup_local_sensor_proxy no-op path failed: %v\n%s", err, noopOut)
|
||||
}
|
||||
if strings.TrimSpace(string(noopOut)) != "" {
|
||||
t.Fatalf("expected silent no-op when no footprint present, got:\n%s", noopOut)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRootInstallUninstallWiresSensorProxyCleanup pins that uninstall_pulse
|
||||
// actually invokes the local sensor-proxy cleanup (the functional test above
|
||||
// only exercises the helper in isolation).
|
||||
func TestRootInstallUninstallWiresSensorProxyCleanup(t *testing.T) {
|
||||
content, err := os.ReadFile(filepath.Join("..", "..", "install.sh"))
|
||||
if err != nil {
|
||||
t.Fatalf("read root install.sh: %v", err)
|
||||
}
|
||||
|
||||
uninstall := extractRootInstallShellFunction(t, "uninstall_pulse")
|
||||
if !strings.Contains(uninstall, "cleanup_local_sensor_proxy") {
|
||||
t.Fatalf("uninstall_pulse does not invoke cleanup_local_sensor_proxy:\n%s", uninstall)
|
||||
}
|
||||
|
||||
script := string(content)
|
||||
for _, needle := range []string{
|
||||
`cleanup_local_sensor_proxy() {`,
|
||||
`local_sensor_proxy_present() {`,
|
||||
`remove_local_sensor_proxy_managed_keys() {`,
|
||||
`# pulse-(managed|proxy)-key$`,
|
||||
} {
|
||||
if !strings.Contains(script, needle) {
|
||||
t.Fatalf("install.sh missing sensor-proxy cleanup contract: %s", needle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootInstallDeployAgentScriptsDeploysSignatureSidecars(t *testing.T) {
|
||||
extractDir := t.TempDir()
|
||||
installDir := t.TempDir()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue