diff --git a/install.sh b/install.sh index 9be656a..35ba28c 100644 --- a/install.sh +++ b/install.sh @@ -175,6 +175,21 @@ fi install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0750 "$DATA_DIR" +# Reassert ownership on the dir's contents. Two cases this catches: +# - re-running the installer over a previous install where the +# service user/group changed +# - the operator ran `qu init` or `qu serve` as root once (easy +# mistake: `sudo qu init` is shorter than the documented +# `sudo -u quptime qu init`). When the daemon runs as root its +# DataDir() resolves to /etc/quptime, so any files it writes land +# here owned by root:root mode 0600 — the systemd service then +# fails with `open node.yaml: permission denied`. +# chown -R only changes ownership, not perms, so file modes set by +# the daemon (0600 for node.yaml, 0700 for keys/) are preserved. +if [ -n "$(ls -A "$DATA_DIR" 2>/dev/null)" ]; then + chown -R "$SERVICE_USER:$SERVICE_GROUP" "$DATA_DIR" +fi + echo "> writing $SERVICE_FILE" cat > "$SERVICE_FILE" <<'EOF' [Unit] @@ -252,11 +267,18 @@ Next steps: # On follower nodes, also set the shared join secret: # Environment=QUPTIME_CLUSTER_SECRET= - b) Or run \`qu init\` once explicitly: + b) Or run \`qu init\` once explicitly. IMPORTANT: run as the + ${SERVICE_USER} user, not root — otherwise node.yaml lands + owned by root and the service can't read it on start. sudo -u ${SERVICE_USER} QUPTIME_DIR=${DATA_DIR} \\ qu init --advertise :9901 + If you already ran it as root and the service is failing + with "permission denied" on node.yaml, repair with: + + sudo chown -R ${SERVICE_USER}:${SERVICE_GROUP} ${DATA_DIR} + 2. Start the service: sudo systemctl start ${SERVICE_NAME} diff --git a/internal/config/paths.go b/internal/config/paths.go index a1f71ba..4c1c128 100644 --- a/internal/config/paths.go +++ b/internal/config/paths.go @@ -16,6 +16,7 @@ import ( "errors" "os" "path/filepath" + "strings" ) // Default file names. Callers should always go through DataDir() so an @@ -55,10 +56,34 @@ func DataDir() string { } // SocketPath returns the unix socket used for local CLI ↔ daemon control. +// +// Resolution order: +// 1. $QUPTIME_SOCKET — explicit operator override +// 2. $RUNTIME_DIRECTORY — set by systemd when the unit declares +// RuntimeDirectory=quptime. This is the path that matters in +// practice: with User=quptime + PrivateTmp=true, the daemon's +// /tmp is namespaced and invisible to the root CLI shell, so a +// /tmp fallback yields "no such file" even though the daemon is +// happily listening. Anchoring on $RUNTIME_DIRECTORY puts the +// socket at /run/quptime/quptime.sock, which is the same inode +// the root-CLI default (/var/run/quptime/…) reaches via the +// /var/run → /run symlink. +// 3. /var/run/quptime/… when euid is 0 (CLI side, packaged installs) +// 4. $XDG_RUNTIME_DIR/quptime/… for user-mode installs +// 5. /tmp/quptime-/… as a last resort func SocketPath() string { if v := os.Getenv("QUPTIME_SOCKET"); v != "" { return v } + if v := os.Getenv("RUNTIME_DIRECTORY"); v != "" { + // systemd may pass multiple colon-separated entries when more + // than one RuntimeDirectory= is declared. Ours is single, but + // be defensive in case a future unit adds more. + if i := strings.IndexByte(v, ':'); i >= 0 { + v = v[:i] + } + return filepath.Join(v, SocketName) + } if os.Geteuid() == 0 { return "/var/run/quptime/" + SocketName }