diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md index 50858dec49..78a1e761de 100644 --- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md +++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md @@ -1168,6 +1168,12 @@ update, profile rollout, command reachability, or fleet-control authority. `shasum`, so checksum verification must fall back through the digest tools actually present on the target platform rather than failing a valid agent download when coreutils is absent. + FreeBSD-family diagnostics are part of that lifecycle too. The rc.d service + has no journal and `daemon(8)` does not capture the child's stdout, so the + rendered service must pass an installer-owned rotating `--log-file` and the + installer's completion hint must name that file. A platform system log is + not a substitute: pfSense ships no `/var/log/messages`, and repointing the + hint at a log that receives no agent output would still strand the operator. Server update planning is part of the same lifecycle contract. The System Updates plan must surface a structured upgrade-readiness verdict before an diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index 71c46fa75c..9b6a318bd3 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -1078,8 +1078,13 @@ artifact-selection behaviour. platform. FreeBSD base ships `sha256(1)` and neither GNU `sha256sum` nor Perl `shasum`, so the unified installer and the MCP installer must fall back through the available digest tool instead of refusing an otherwise correct - download. The shared installer helper owns that fallback so every download - (agent, typed helper, action runner) is verified consistently. + download. The shared installer helper owns that fallback so every download + (agent, typed helper, action runner) is verified consistently. + FreeBSD and pfSense installs must likewise leave the agent with a durable, + installer-owned rotating log. rc.d has no journal and `daemon(8)` discards + the child's stdout, so the rendered rc.d service must carry the agent's + `--log-file` and the install summary must point at that file rather than + `/var/log/messages`, which pfSense does not ship. The shell installer must disclose `--enable-commands` as Pulse command execution, disabled by default, and must name both Patrol actions and Proxmox LXC Docker inventory as the operator-visible reasons to enable it. diff --git a/scripts/install.sh b/scripts/install.sh index af871e1b08..fe2c62a9cb 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -6821,6 +6821,13 @@ if [[ "$TRUENAS" == true ]]; then mkdir -p "$TRUENAS_STATE_DIR" mkdir -p "$TRUENAS_LOG_DIR" + # FreeBSD rc.d has no journal and daemon(8) does not capture the child's + # stdout, so the agent's own rotating writer must own its log. On Linux the + # systemd unit captures stdout via journald instead. + if [[ "$(uname -s)" == "FreeBSD" ]]; then + AGENT_LOG_FILE="${TRUENAS_LOG_DIR}/${AGENT_NAME}.log" + fi + TRUENAS_STORED_BINARY="$TRUENAS_STATE_DIR/${BINARY_NAME}" # Move binary to persistent storage location @@ -6958,7 +6965,7 @@ EOF log_info "Logs: tail -f ${TRUENAS_LOG_FILE}" elif [[ "$(uname -s)" == "FreeBSD" ]]; then log_info "Service: $TRUENAS_SERVICE_STORAGE (symlinked to rc.d)" - log_info "Logs: tail -f /var/log/messages" + log_info "Logs: tail -f ${AGENT_LOG_FILE}" fi log_info "" log_info "The Init/Shutdown task ensures the agent survives TrueNAS upgrades." @@ -7023,6 +7030,12 @@ if [[ "$OS" == "freebsd" ]] || [[ -f /etc/rc.subr ]]; then RCSCRIPT="/usr/local/etc/rc.d/${AGENT_NAME}" log_info "Configuring FreeBSD rc.d service at $RCSCRIPT..." + # FreeBSD rc.d has no journal, and daemon(8) does not capture the child's + # stdout, so the agent's own rotating writer must own its log. Without this + # the agent logs are lost and the completion hint below would point at a + # system log that never receives them (and that pfSense does not even ship). + AGENT_LOG_FILE="${STATE_DIR}/logs/${AGENT_NAME}.log" + # Build command line args ensure_runtime_token_file "$STATE_DIR" clear_proxmox_state_if_needed @@ -7050,9 +7063,9 @@ BOOTEOF # Stop existing agent if running restart_sysv_agent_service "$RCSCRIPT" - complete_installation_flow "$STATE_DIR" "Installation complete! Agent is running." "Upgrade complete! Agent restarted with new configuration." "tail -f /var/log/messages" + complete_installation_flow "$STATE_DIR" "Installation complete! Agent is running." "Upgrade complete! Agent restarted with new configuration." "tail -f ${AGENT_LOG_FILE}" log_info "To check status: $RCSCRIPT status" - log_info "To view logs: tail -f /var/log/messages" + log_info "To view logs: tail -f ${AGENT_LOG_FILE}" exit 0 fi diff --git a/scripts/installtests/install_sh_test.go b/scripts/installtests/install_sh_test.go index c5e54dea7f..128a240a6f 100644 --- a/scripts/installtests/install_sh_test.go +++ b/scripts/installtests/install_sh_test.go @@ -2658,6 +2658,83 @@ func TestInstallSHFreeBSDRendererUsesDaemonSupervisorPidfile(t *testing.T) { } } +// TestInstallSHFreeBSDAgentLogsUseRotatingWriter pins the FreeBSD half of the +// log-capture fix for issue #2123: rc.d has no journal and daemon(8) does not +// capture the child's stdout, so the agent must own a rotating --log-file and +// the completion hint must point at it. pfSense has no /var/log/messages, so +// the old system-log hint is removed entirely rather than repointed. +func TestInstallSHFreeBSDAgentLogsUseRotatingWriter(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{ + `AGENT_LOG_FILE="${STATE_DIR}/logs/${AGENT_NAME}.log"`, + `AGENT_LOG_FILE="${TRUENAS_LOG_DIR}/${AGENT_NAME}.log"`, + `complete_installation_flow "$STATE_DIR" "Installation complete! Agent is running." "Upgrade complete! Agent restarted with new configuration." "tail -f ${AGENT_LOG_FILE}"`, + `log_info "To view logs: tail -f ${AGENT_LOG_FILE}"`, + } + for _, needle := range required { + if !strings.Contains(script, needle) { + t.Fatalf("install.sh missing FreeBSD rotating agent log wiring: %s", needle) + } + } + if strings.Contains(script, "tail -f /var/log/messages") { + t.Fatal("install.sh still points FreeBSD/pfSense users at /var/log/messages, which does not exist on pfSense") + } +} + +// TestInstallSHFreeBSDBuildArgsPassesLogFile verifies the FreeBSD rc.d service +// args actually carry --log-file once AGENT_LOG_FILE is set. +func TestInstallSHFreeBSDBuildArgsPassesLogFile(t *testing.T) { + script := ` + PULSE_URL="http://pulse.local:7655" + PULSE_TOKEN="" + INTERVAL="30s" + INTERVAL_EXPLICIT="false" + ENABLE_HOST="true" + HOST_EXPLICIT="false" + ENABLE_DOCKER="" + DOCKER_EXPLICIT="false" + ENABLE_KUBERNETES="" + KUBERNETES_EXPLICIT="false" + KUBECONFIG_PATH="" + ENABLE_PROXMOX="" + PROXMOX_EXPLICIT="false" + PROXMOX_TYPE="" + INSECURE="false" + ENABLE_COMMANDS="false" + ENROLL="false" + HEALTH_ADDR="" + HEALTH_ADDR_SET="false" + AGENT_ID="" + HOSTNAME_OVERRIDE="" + STATE_DIR="/var/lib/pulse-agent" + CURL_CA_BUNDLE="" + KUBE_INCLUDE_ALL_PODS="false" + KUBE_INCLUDE_ALL_DEPLOYMENTS="false" + DISK_EXCLUDES=() + DISK_INCLUDES=() + RUNTIME_TOKEN_FILE="" + AGENT_LOG_FILE="/var/lib/pulse-agent/logs/pulse-agent.log" +` + extractInstallShellFunction(t, "build_exec_arg_items") + ` +` + extractInstallShellFunction(t, "join_exec_arg_items") + ` +` + extractInstallShellFunction(t, "build_exec_args") + ` + build_exec_args + printf 'EXEC_ARGS=%s\n' "$EXEC_ARGS" + ` + out, err := exec.Command("bash", "-c", script).CombinedOutput() + if err != nil { + t.Fatalf("bash: %v\n%s", err, out) + } + got := string(out) + if !strings.Contains(got, "--log-file /var/lib/pulse-agent/logs/pulse-agent.log") { + t.Fatalf("FreeBSD service args missing rotating --log-file:\n%s", got) + } +} + func TestInstallSHUsesCanonicalCompletionHelper(t *testing.T) { content, err := os.ReadFile(repoFile("scripts", "install.sh")) if err != nil { @@ -2676,7 +2753,7 @@ func TestInstallSHUsesCanonicalCompletionHelper(t *testing.T) { `complete_installation_flow "$UNRAID_STORAGE_DIR" "Installation complete! Agent is running." "Upgrade complete! Agent is running." "tail -f ${AGENT_LOG_FILE}"`, `complete_installation_flow "$STATE_DIR" "Installation complete! Agent is running." "Upgrade complete! Agent is running." "tail -f ${AGENT_LOG_FILE}"`, `complete_installation_flow "$TRUENAS_STATE_DIR" "Installation complete! Agent is running." "Upgrade complete! Agent is running." ""`, - `complete_installation_flow "$STATE_DIR" "Installation complete! Agent is running." "Upgrade complete! Agent restarted with new configuration." "tail -f /var/log/messages"`, + `complete_installation_flow "$STATE_DIR" "Installation complete! Agent is running." "Upgrade complete! Agent restarted with new configuration." "tail -f ${AGENT_LOG_FILE}"`, `complete_installation_flow "$STATE_DIR" "Installation complete! Agent is running." "Upgrade complete! Agent restarted with new configuration." "journalctl -u ${AGENT_NAME} --no-pager -n 20"`, `complete_installation_flow "$STATE_DIR" "Installation complete! Agent is running." "Upgrade complete! Agent restarted with new configuration." "tail -f /var/log/${AGENT_NAME}.log"`, }