#!/bin/bash
#
# weekly_maintenance.sh – perform routine Perforce cleanup and notifications.
#
# This script performs a series of maintenance tasks against a Helix Core
# installation using the SDP environment. It logs all output to a file
# and emails failures to the configured MAILTO address. Client unloading
# is capped at 200 workspaces per run without a threshold alert.
# Fail fast on errors, unset variables and pipeline failures.
set -uo pipefail
# Determine the SDP instance either from the first positional parameter or
# the environment. Require it to be set to avoid accidental misuse.
P4INSTANCE="${1:-${P4INSTANCE:-}}"
if [[ -z "$P4INSTANCE" ]]; then
echo "P4PORT=${P4PORT:-unknown}: Instance parameter not supplied." >&2
echo "You must supply the Perforce instance as a parameter to this script or set \$P4INSTANCE in the environment."
exit 1
fi
# Initialize pyenv so scripts use the correct Python version.
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"
# Source SDP environment variables and ensure we can talk to Perforce.
if ! source "/p4/common/bin/p4_vars" "$P4INSTANCE"; then
echo "P4PORT=${P4PORT:-unknown}: Unable to source p4_vars for instance $P4INSTANCE" >&2
exit 1
fi
# sdp_structured_log.sh is standalone (no backup_functions.sh dependency) and
# only needs $LOGS, already exported by p4_vars above.
# shellcheck disable=SC1091
[[ -r /p4/common/bin/sdp_structured_log.sh ]] && source /p4/common/bin/sdp_structured_log.sh
declare -F sdp_log_init >/dev/null && sdp_log_init edge_maintenance
# Prepare our log file and client names. Use the hostname and server name for uniqueness.
LOG="/p4/sdp/Maintenance/${HOSTNAME}_${P4SERVER}_log.txt"
P4CLIENT="${HOSTNAME}_unload"
# Ensure the maintenance working directory exists and change into it for relative paths.
MAINT_DIR="/p4/sdp/Maintenance"
if [[ ! -d "$MAINT_DIR" ]]; then
echo "P4PORT=${P4PORT:-unknown}: Maintenance directory $MAINT_DIR does not exist." >&2
exit 1
fi
# Both weekly entry points share one instance lock; each run gets its own
# reports and scratch files. The server.locks cleanup below is intentional.
source "$MAINT_DIR/maintenance_workspace.sh" || exit 1
maintenance_workspace_init "$P4INSTANCE" || {
workspace_status=$?
if (( workspace_status == 75 )); then exit 0; fi
exit "$workspace_status"
}
# Acquire the instance lock before replacing the shared run log. A competing
# invocation must not rotate the active owner's diagnostics.
if declare -F sdp_prepare_run_log >/dev/null; then
sdp_prepare_run_log "$LOG" || exit 1
exec >"$LOG" 2>&1 || exit 1
else
exec >>"$LOG" 2>&1 || exit 1
fi
printf 'Maintenance run directory: %s run_id=%s\n' "$SDP_MAINTENANCE_WORKDIR" "${SDP_RUN_ID:-unavailable}"
# Helper function to timestamp and append a message to the log.
log_msg() {
local msg="${1:-}"
printf '%s %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$msg" >>"$LOG"
}
# Tracks whether any step below actually failed, so the end-of-run email
# only fires on a real problem instead of on every single run (it was
# unconditional before - "Weekly maintenance log" landed in MAILTO's inbox
# every night regardless of outcome). Also mirrors into the structured
# jsonl record via sdp_set_status/sdp_set_error, since none of the `|| true`
# guards below were ever reporting failure to it either.
HAD_ERROR=0
run_step() {
local desc="$1"
shift
log_msg "$desc"
"$@" >>"$LOG" 2>&1
local rc=$?
if (( rc != 0 )); then
log_msg "ERROR: $desc failed (exit $rc)"
HAD_ERROR=1
declare -F sdp_set_status >/dev/null && sdp_set_status failure
declare -F sdp_set_error >/dev/null && sdp_set_error "$desc failed (exit $rc)"
fi
return "$rc"
}
# Start logging.
log_msg "--- Weekly maintenance started for instance $P4INSTANCE (P4PORT=${P4PORT:-unknown}, run_id=${SDP_RUN_ID:-unavailable}) ---"
# Authentication failures must be visible and stop all cleanup work.
run_step "Authenticating with p4login" /p4/common/bin/p4login || exit 1
# Remove any leftover server lock files. This can prevent DB replays from
# succeeding if the previous run terminated uncleanly.
run_step "Removing server lock files from /p4/${P4INSTANCE}/root/server.locks" \
rm -rf "/p4/${P4INSTANCE}/root/server.locks"
# Generate lists of inactive clients. accessdates.py produces clients.txt
# based on the configured number of weeks.
REPORT_OK=0
if run_step "Generating client inactivity report via accessdates.py" \
accessdates.py "$P4INSTANCE" --client-snapshot client_snapshot.json; then
REPORT_OK=1
fi
# Count the number of clients to be unloaded and decide what to do. Use
# wc -l with a redirection to avoid counting the filename itself. Fall back
# to zero if the file is missing.
client_count=0
if (( REPORT_OK )) && [[ -f clients.txt ]]; then
client_count=$(wc -l < clients.txt || echo 0)
fi
log_msg "Inactive client count: $client_count"
declare -F sdp_add_field >/dev/null && sdp_add_field inactive_client_count "$client_count"
# Unload at most 200 clients per run; leave any remainder for later runs.
if (( client_count > 0 )); then
unload_count=$(( client_count < 200 ? client_count : 200 ))
run_step "Unloading up to $unload_count clients via unload_clients.py" \
unload_clients.py "$P4INSTANCE" --limit 200 --clients-file client_snapshot.json
fi
# Notify users about pending client deletions. This script emails owners of
# workspaces that will be removed soon. Only run once per maintenance window.
# run_step "Running email_pending_client_deletes.py" email_pending_client_deletes.py "$P4INSTANCE"
# Remove empty pending changelists. These accumulate over time and can slow
# down server operations.
run_step "Removing empty pending changelists via remove_empty_pending_changes.py" \
remove_empty_pending_changes.py "$P4INSTANCE"
# Finish logging. Only email MAILTO when something actually failed - the
# structured jsonl record (emitted automatically on exit by sdp_log_init's
# trap, status/error set by run_step above) is the routine, non-noisy
# record of every run; this email is reserved for the cases that need a
# human to look.
log_msg "--- Weekly maintenance completed run_id=${SDP_RUN_ID:-unavailable} ---"
if (( HAD_ERROR )) && [[ -n "${MAILTO:-}" ]]; then
if ! mail -s "${HOSTNAME} ${P4SERVER} P4PORT=${P4PORT:-unknown} Weekly maintenance FAILED" "$MAILTO" <"$LOG"; then
log_msg "ERROR: Unable to send maintenance failure notification"
fi
fi
exit "$HAD_ERROR"
| # | Change | User | Description | Committed | |
|---|---|---|---|---|---|
| #19 | 34004 | Russell C. Jackson (Rusty) |
Improve SDP backup/recovery, Maintenance, logging and cloud operation. Add owned checkpoint locks, recovery-aware retention, preserved database switches, bounded cleanup and coordinated AWS/Azure snapshots. Fix service-user password initialization, edge staging, standby promotion state parsing, proxy cache setup, typed depot verification and historical-server configuration/error handling. Remove unused OTP programs; keep Windows removed, server.locks cleanup and the journal-count condition unchanged. Add no global shell strict modes. Update operational documentation and add reproducible mkdirs.sh/mkdirs.cfg container assurance. Validate Ubuntu 26.04 and UBI 8.10/9.8/10.2 across both Python runtimes: 44 stages; 428 offline tests per runtime/OS, zero offline skips; 19 deployed-archive comparisons per combination. Three six-node topology runs pass all 30 recovery/failure scenarios. P4D 2023.1 through 2026.1 on UBI 8/9 pass all 14 cells and 98 scenarios. Static, documentation and provenance checks pass. Containers do not certify every fleet kernel, patch build or integration. Tested source manifest SHA-256: e1a23e17b8ced8c1148d9a250589977c6f6e98606bf2a81dc3ff5d7c909e3695 Deployed comparison archive SHA-256: 1bd23217356ee3808e040d39aea5be9519bda7c8f2b4924656a70aff8ad7293a |
||
| #18 | 34003 | Russell C. Jackson (Rusty) |
Improve SDP diagnostics and correct maintenance/recovery failures. Add durable logs and explicit failure handling across operational, Maintenance, setup, trigger, snapshot and download scripts. Correct retention selection, restore preflights, environment setup and command/API handling. Include offline regression coverage and the review report with deferred improvement proposals. Include requested journal archiving and automatic acceptance of new SSH host keys for remote rsync. Preserve the NumJnls > 2 threshold and existing shell options. Remove Windows scripts as requested. Validation: full review suite 138 passed, 8 skipped; subsequent rsync/logging suite 29 passed. Shell/Python/JSON static checks passed; no live operations run. |
||
| #17 | 33803 | Russell C. Jackson (Rusty) | Cap maintenance client unloading at 200 workspaces per run without threshold alerts; pass the SDP instance and limit to unload_clients.py. | ||
| #16 | 33799 | Russell C. Jackson (Rusty) | Include P4PORT in cron diagnostics and SDP alert logging | ||
| #15 | 33760 | Russell C. Jackson (Rusty) | Commenting out email warnings for now. | ||
| #14 | 33526 | Russell C. Jackson (Rusty) |
Fix unconditional maintenance emails; add jsonl structured logging across Maintenance/ - Maintenance/maintenance and Maintenance/edge_maintenance (run daily via cron, despite the "weekly" naming) were emailing the full run log on every single run, success or not, because every step was guarded with `|| true` and never reported failure anywhere. Added a run_step helper that tracks real failures and wires them into sdp_set_status/ sdp_set_error; the end-of-run email now only fires when something actually failed (subject changed to "... FAILED" to make that obvious). - Every other script in Maintenance/ (39 Python scripts + create_p4_filelist.sh + email.sh; sdputils.py excluded as a pure library with no __main__) now emits the same jsonl structured-logging record as the cron scripts in common/bin, via sdp_structured_log.py's run() context manager (auto-emits success/failure on exit, sys.exit(N), or unhandled exception) or, for the two shell scripts, sdp_structured_log.sh's sdp_log_init (auto ERR/EXIT trap coverage). - remove_empty_pending_changes.py: the initial pending-changes listing failure path did a bare `return` (swallowed by the auto-emit machinery, since a normal return maps to exit code 0/success); now returns 1, and per-change delete failures inside the loop also flip the overall exit code, so a partially-failed run is correctly logged as a failure instead of a silent success. - pymail.py: usage() called a bare sys.exit() (exit code 0) for both the -h/help path AND real bad-invocation paths (missing required args, getopt errors) - meaning a bad invocation reported success both to the OS and to the structured log. usage() now takes an explicit code (default 0, preserving -h's exit-0 convention); the two real error call sites pass a nonzero code. |
||
| #13 | 33459 | Russell C. Jackson (Rusty) | Fix cron mail noise from p4login output and unconditional success emails; add sdp_log_init jsonl logging (with matching log rotation) to remaining SDP maintenance scripts; support multiple limits groups in update_limits.py; exempt super users from keep_group_unset.py trigger; remove dead crontab entries | ||
| #12 | 32442 | Russell C. Jackson (Rusty) | Added the pyenv environment set up to the maintenance scripts. | ||
| #11 | 31928 | Russell C. Jackson (Rusty) | Removed set -e from scripts. | ||
| #10 | 31836 | Russell C. Jackson (Rusty) | Added limit on the number of clients to unload | ||
| #9 | 31545 | Russell C. Jackson (Rusty) | Removed delete of unloaded clients from edge maintenance. | ||
| #8 | 30939 | Russell C. Jackson (Rusty) | Added delete_unload_clients.py to maintenance | ||
| #7 | 29744 | Russell C. Jackson (Rusty) | Redirect output to dev null to manage the size of the log file. | ||
| #6 | 29104 | Russell C. Jackson (Rusty) | Update remove_empty to use ztag and added the script to the edge_maintenance. | ||
| #5 | 28680 | Russell C. Jackson (Rusty) | Changed sh to bash | ||
| #4 | 28419 | Russell C. Jackson (Rusty) | Fixed edge maintenance log path. | ||
| #3 | 24713 | Russell C. Jackson (Rusty) | Removed specific client data. | ||
| #2 | 24675 | Russell C. Jackson (Rusty) |
Fixed bugs in sdputils.py and scripts using it. Converted to standard 2 space spacing, removed copyright stuff. |
||
| #1 | 24075 | Russell C. Jackson (Rusty) | Added maintenance and edge_maintenance to crontab, created edge_maintenance and updated maintenance. |