mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Run the QNAP agent from the data volume instead of the RAM-backed root
The installer staged the download in /tmp and installed the runtime binary to /usr/local/bin, both on the small RAM-backed QTS/QuTS hero root, and the boot wrapper copied 34MiB back onto that root at every boot. Roots without ~50MiB of headroom could not install at all, and setting TMPDIR only moved the staging half of the requirement. QNAP's own QPKG packages execute from the data volume, so do the same: relocate the install dir to the data volume's state dir before the preflight and download, default TMPDIR there too, skip the boot-time self-copy when the stored and runtime binaries are one file, and remove a pre-relocation runtime copy from /usr/local/bin to give that space back. Split layouts with an operator-supplied state dir keep the copy semantics. The rendered wrapper is exercised in both layouts by the installer tests. Refs #1617 Contract-Neutral: Refs #1617: QNAP installer layout fix with its deployment-installability contract clause staged in this commit; residual proof policies for unrelated boundaries do not apply to this shell-only change
This commit is contained in:
@@ -3330,7 +3330,13 @@ the writable data volume, write a flash-backed `autorun.sh` block that waits
|
||||
for that volume before launching the wrapper, recover the same state during
|
||||
uninstall, and keep the persisted boot copy aligned with updater-owned runtime
|
||||
binary replacements instead of assuming `/usr/local/bin` survives reboot on
|
||||
QTS/QuTS hero.
|
||||
QTS/QuTS hero. Because that root is a small RAM-backed volume that can lack
|
||||
the headroom to stage or hold the agent at all, the installer must stage,
|
||||
install, and run the agent binary from the data volume itself, defaulting the
|
||||
staging `TMPDIR` there when the operator has not chosen one, reclaiming any
|
||||
pre-relocation runtime copy left under `/usr/local/bin`, and keeping the
|
||||
boot-time runtime copy only for split layouts where an operator-supplied
|
||||
state directory separates the stored and runtime binaries.
|
||||
Before any agent artifact download or replacement, that boundary must also
|
||||
prove adequate space on the effective temporary and install filesystems,
|
||||
deduplicating the requirement when both paths share a filesystem and honoring
|
||||
|
||||
+36
-3
@@ -1651,8 +1651,12 @@ wait_for_file "${stored_binary}"
|
||||
pkill -x "pulse-agent" 2>/dev/null || true
|
||||
sleep 2
|
||||
|
||||
mkdir -p "$(dirname "$runtime_binary")" 2>/dev/null || true
|
||||
cp "${stored_binary}" "${runtime_binary}"
|
||||
# When the runtime binary lives on the data volume it IS the stored binary;
|
||||
# only a split layout needs the boot-time copy back onto the root.
|
||||
if [ "${stored_binary}" != "${runtime_binary}" ]; then
|
||||
mkdir -p "$(dirname "$runtime_binary")" 2>/dev/null || true
|
||||
cp "${stored_binary}" "${runtime_binary}"
|
||||
fi
|
||||
chmod +x "${runtime_binary}"${service_env_lines}
|
||||
|
||||
# Watchdog loop: restart agent if it exits.
|
||||
@@ -3674,6 +3678,25 @@ elif [[ "$(uname -s)" == "FreeBSD" ]] && [[ -d /data ]] && ! is_install_dir_writ
|
||||
log_info "Immutable filesystem detected (read-only /usr/local/bin). Using $TRUENAS_STATE_DIR for installation."
|
||||
fi
|
||||
|
||||
# QNAP QTS/QuTS hero: the root filesystem is a small RAM-backed volume that is
|
||||
# rebuilt on every boot, so staging to /tmp and installing to /usr/local/bin
|
||||
# can both fail on space and never persist anyway (issue #1617). QNAP's own
|
||||
# QPKG packages execute from the data volume, so stage, install, and run the
|
||||
# agent from there.
|
||||
if [[ "$(uname -s)" == "Linux" ]] && { [[ -f /sbin/getcfg ]] || [[ -f /etc/config/qpkg.conf ]]; }; then
|
||||
QNAP_EARLY_VOL=$(detect_qnap_data_volume || true)
|
||||
if [[ -n "$QNAP_EARLY_VOL" ]]; then
|
||||
INSTALL_DIR="${QNAP_EARLY_VOL}/.pulse-agent"
|
||||
if [[ -z "${TMPDIR:-}" ]]; then
|
||||
QNAP_STAGING_TMPDIR="${QNAP_EARLY_VOL}/.pulse-agent/tmp"
|
||||
if mkdir -p "$QNAP_STAGING_TMPDIR" 2>/dev/null; then
|
||||
export TMPDIR="$QNAP_STAGING_TMPDIR"
|
||||
fi
|
||||
fi
|
||||
log_info "QNAP detected (RAM-backed root). Staging and installing under ${INSTALL_DIR}."
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Preflight-Only Mode ---
|
||||
if [[ "$PREFLIGHT_ONLY" == "true" ]]; then
|
||||
json_event "preflight" "checking" "Running preflight checks"
|
||||
@@ -4200,10 +4223,20 @@ if [[ -f /sbin/getcfg ]] || [[ -f /etc/config/qpkg.conf ]]; then
|
||||
mkdir -p "$STATE_DIR"
|
||||
|
||||
# Copy binary to persistent storage and keep the runtime copy executable.
|
||||
cp "${RUNTIME_BINARY}" "$QNAP_STORED_BINARY"
|
||||
# With the data-volume install dir these are the same file; the copy only
|
||||
# applies when a custom STATE_DIR separates them.
|
||||
if [[ "$RUNTIME_BINARY" != "$QNAP_STORED_BINARY" ]]; then
|
||||
cp "${RUNTIME_BINARY}" "$QNAP_STORED_BINARY"
|
||||
fi
|
||||
chmod +x "$QNAP_STORED_BINARY"
|
||||
chmod +x "$RUNTIME_BINARY"
|
||||
|
||||
# A pre-relocation install left its runtime copy on the RAM-backed root;
|
||||
# reclaim that space now that the agent runs from the data volume.
|
||||
if [[ "$RUNTIME_BINARY" != "/usr/local/bin/${BINARY_NAME}" ]]; then
|
||||
rm -f "/usr/local/bin/${BINARY_NAME}"
|
||||
fi
|
||||
|
||||
log_info "Installed binary to ${QNAP_STORED_BINARY} (persistent) and ${RUNTIME_BINARY} (runtime)..."
|
||||
|
||||
# Log to the data volume with the agent's rotating writer; the RAM-backed
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
package installtests
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// The QNAP root filesystem is a small RAM-backed volume rebuilt on every
|
||||
// boot, so the installer must stage, install, and run the agent from the
|
||||
// data volume instead. Refs #1617.
|
||||
func TestInstallSHRelocatesQNAPInstallToDataVolume(t *testing.T) {
|
||||
content, err := os.ReadFile(repoFile("scripts", "install.sh"))
|
||||
if err != nil {
|
||||
t.Fatalf("read install.sh: %v", err)
|
||||
}
|
||||
|
||||
script := string(content)
|
||||
required := []string{
|
||||
`QNAP_EARLY_VOL=$(detect_qnap_data_volume || true)`,
|
||||
`INSTALL_DIR="${QNAP_EARLY_VOL}/.pulse-agent"`,
|
||||
`export TMPDIR="$QNAP_STAGING_TMPDIR"`,
|
||||
`if [[ "$RUNTIME_BINARY" != "$QNAP_STORED_BINARY" ]]; then`,
|
||||
`rm -f "/usr/local/bin/${BINARY_NAME}"`,
|
||||
}
|
||||
for _, needle := range required {
|
||||
if !strings.Contains(script, needle) {
|
||||
t.Fatalf("install.sh missing QNAP data-volume install handling: %s", needle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// With the data-volume layout the stored and runtime binaries are one file;
|
||||
// the rendered watchdog must start the agent without a boot-time self-copy.
|
||||
func TestRenderedQNAPWatchdogRunsUnifiedStoredRuntimeBinary(t *testing.T) {
|
||||
content, err := os.ReadFile(repoFile("scripts", "install.sh"))
|
||||
if err != nil {
|
||||
t.Fatalf("read install.sh: %v", err)
|
||||
}
|
||||
|
||||
script := string(content)
|
||||
start := strings.Index(script, "write_qnap_wrapper_script() {")
|
||||
if start < 0 {
|
||||
t.Fatal("install.sh missing QNAP wrapper renderer")
|
||||
}
|
||||
endOffset := strings.Index(script[start:], "\nappend_qnap_autorun_block() {")
|
||||
if endOffset < 0 {
|
||||
t.Fatal("could not isolate QNAP wrapper renderer")
|
||||
}
|
||||
renderer := script[start : start+endOffset]
|
||||
|
||||
tempDir := t.TempDir()
|
||||
stateDir := filepath.Join(tempDir, "state")
|
||||
logDir := filepath.Join(stateDir, "logs")
|
||||
wrapperPath := filepath.Join(stateDir, "start-pulse-agent.sh")
|
||||
binaryPath := filepath.Join(stateDir, "pulse-agent")
|
||||
startsPath := filepath.Join(tempDir, "agent-starts")
|
||||
mockBinDir := filepath.Join(tempDir, "bin")
|
||||
|
||||
for _, dir := range []string{stateDir, mockBinDir} {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
t.Fatalf("create test directory: %v", err)
|
||||
}
|
||||
}
|
||||
agentScript := "#!/bin/sh\n" +
|
||||
"echo \"$$\" >> \"" + startsPath + "\"\n" +
|
||||
"trap 'exit 0' INT TERM HUP\n" +
|
||||
"while :; do sleep 1; done\n"
|
||||
if err := os.WriteFile(binaryPath, []byte(agentScript), 0o755); err != nil {
|
||||
t.Fatalf("write fake agent: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(mockBinDir, "pkill"), []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil {
|
||||
t.Fatalf("write fake pkill: %v", err)
|
||||
}
|
||||
|
||||
harness := renderer + `
|
||||
AGENT_NAME=pulse-agent
|
||||
SHELL_EXPORT_LINES=""
|
||||
EXEC_ARGS=""
|
||||
write_qnap_wrapper_script "$1" "$2" "$3" "$4" "$5"
|
||||
`
|
||||
render := exec.Command("bash", "-c", harness, "_", wrapperPath, binaryPath, binaryPath, logDir, stateDir)
|
||||
if output, err := render.CombinedOutput(); err != nil {
|
||||
t.Fatalf("render QNAP wrapper: %v\n%s", err, output)
|
||||
}
|
||||
|
||||
rendered, err := os.ReadFile(wrapperPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read rendered wrapper: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(rendered), `if [ "`+binaryPath+`" != "`+binaryPath+`" ]; then`) {
|
||||
t.Fatal("rendered wrapper lost the unified-layout copy guard")
|
||||
}
|
||||
|
||||
watchdog := exec.Command("sh", wrapperPath)
|
||||
watchdog.Env = []string{"PATH=" + mockBinDir + ":/usr/bin:/bin"}
|
||||
if err := watchdog.Start(); err != nil {
|
||||
t.Fatalf("start watchdog: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if watchdog.Process != nil {
|
||||
_ = watchdog.Process.Signal(syscall.SIGTERM)
|
||||
_, _ = watchdog.Process.Wait()
|
||||
}
|
||||
})
|
||||
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
if _, err := os.Stat(startsPath); err == nil {
|
||||
return
|
||||
}
|
||||
time.Sleep(25 * time.Millisecond)
|
||||
}
|
||||
t.Fatal("timed out waiting for the unified-layout watchdog to start the agent")
|
||||
}
|
||||
Reference in New Issue
Block a user