200 lines
6.1 KiB
Bash
Executable File
200 lines
6.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -Eeuo pipefail
|
|
|
|
umask 077
|
|
|
|
APP_ROOT=/opt/kaidi
|
|
BAOTA_ROOT=/www/wwwroot/kaidi
|
|
CONFIG_ROOT=/etc/kaidi
|
|
STATE_ROOT=/var/lib/kaidi
|
|
UPDATE_STATE_ROOT=/var/lib/kaidi-update
|
|
LOG_ROOT=/var/log/kaidi
|
|
FIRST_LOGIN_FILE=/root/kaidi-first-login.txt
|
|
BACKUP_ROOT=${KAIDI_PURGE_BACKUP_ROOT:-/root/kaidi-reinstall-backups}
|
|
CONFIRMATION=${KAIDI_PURGE_CONFIRM:-}
|
|
REQUIRED_CONFIRMATION=DELETE_LOCAL_KAIDI_INSTALLATION
|
|
SERVICE_USER=kaidi
|
|
SERVICE_GROUP=kaidi
|
|
BACKUP_ARCHIVE=
|
|
|
|
log() { printf '[kaidi-purge] %s\n' "$*"; }
|
|
die() { printf '[kaidi-purge] ERROR: %s\n' "$*" >&2; exit 1; }
|
|
|
|
systemd_available() {
|
|
command -v systemctl >/dev/null 2>&1 && [ -d /run/systemd/system ]
|
|
}
|
|
|
|
validate_inputs() {
|
|
[ "$(id -u)" -eq 0 ] || die "Run with sudo or as root"
|
|
[ "$(uname -s)" = Linux ] || die "The purge script only supports Linux"
|
|
[ "$CONFIRMATION" = "$REQUIRED_CONFIRMATION" ] \
|
|
|| die "Set KAIDI_PURGE_CONFIRM=$REQUIRED_CONFIRMATION to confirm local removal"
|
|
case "$BACKUP_ROOT" in
|
|
/*) ;;
|
|
*) die "KAIDI_PURGE_BACKUP_ROOT must be an absolute path" ;;
|
|
esac
|
|
case "$BACKUP_ROOT/" in
|
|
"$APP_ROOT/"*|"$BAOTA_ROOT/"*|"$CONFIG_ROOT/"*|"$STATE_ROOT/"*|\
|
|
"$UPDATE_STATE_ROOT/"*|"$LOG_ROOT/"*)
|
|
die "The recovery backup must be outside every managed Kaidi directory"
|
|
;;
|
|
esac
|
|
[ ! -L "$BACKUP_ROOT" ] || die "The recovery backup root must not be a symbolic link"
|
|
}
|
|
|
|
stop_systemd_units() {
|
|
local unit
|
|
systemd_available || return 0
|
|
for unit in kaidi-update.path kaidi-update.service kaidi-finance.service; do
|
|
if systemctl cat "$unit" >/dev/null 2>&1; then
|
|
systemctl stop "$unit" >/dev/null 2>&1 \
|
|
|| die "Failed to stop $unit"
|
|
systemctl disable "$unit" >/dev/null 2>&1 || true
|
|
fi
|
|
done
|
|
}
|
|
|
|
related_pids() {
|
|
local proc pid command_line
|
|
for proc in /proc/[0-9]*; do
|
|
[ -r "$proc/cmdline" ] || continue
|
|
pid=${proc##*/}
|
|
[ "$pid" != "$$" ] && [ "$pid" != "$PPID" ] || continue
|
|
command_line=$(tr '\000' ' ' < "$proc/cmdline" 2>/dev/null || true)
|
|
case "$command_line" in
|
|
*"$APP_ROOT/"*|*"$BAOTA_ROOT/"*) printf '%s\n' "$pid" ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
stop_managed_processes() {
|
|
local deadline
|
|
local -a pids=()
|
|
mapfile -t pids < <(related_pids)
|
|
if [ "${#pids[@]}" -gt 0 ]; then
|
|
log "Stopping remaining Kaidi processes: ${pids[*]}"
|
|
kill -TERM "${pids[@]}" 2>/dev/null || true
|
|
fi
|
|
deadline=$((SECONDS + 5))
|
|
while [ "$SECONDS" -lt "$deadline" ]; do
|
|
mapfile -t pids < <(related_pids)
|
|
[ "${#pids[@]}" -gt 0 ] || return 0
|
|
sleep 1
|
|
done
|
|
kill -KILL "${pids[@]}" 2>/dev/null || true
|
|
sleep 1
|
|
mapfile -t pids < <(related_pids)
|
|
[ "${#pids[@]}" -eq 0 ] \
|
|
|| die "A process manager is restarting Kaidi; remove the Kaidi project from Baota and run this command again"
|
|
}
|
|
|
|
create_recovery_backup() {
|
|
local path relative temporary timestamp
|
|
local -a paths=()
|
|
for path in \
|
|
"$CONFIG_ROOT" \
|
|
"$STATE_ROOT" \
|
|
"$UPDATE_STATE_ROOT/backups" \
|
|
"$UPDATE_STATE_ROOT/failed" \
|
|
"$UPDATE_STATE_ROOT/transactions" \
|
|
"$FIRST_LOGIN_FILE"; do
|
|
if [ -e "$path" ] || [ -L "$path" ]; then
|
|
relative=${path#/}
|
|
paths+=("$relative")
|
|
fi
|
|
done
|
|
if [ "${#paths[@]}" -eq 0 ]; then
|
|
log "No local configuration or data needs a recovery backup"
|
|
return 0
|
|
fi
|
|
install -d -o root -g root -m 0700 "$BACKUP_ROOT"
|
|
timestamp=$(date -u +%Y%m%dT%H%M%SZ)
|
|
BACKUP_ARCHIVE="$BACKUP_ROOT/kaidi-local-state-$timestamp.tar.gz"
|
|
temporary="$BACKUP_ARCHIVE.next.$$"
|
|
log "Creating a root-only recovery backup at $BACKUP_ARCHIVE"
|
|
tar -czf "$temporary" -C / -- "${paths[@]}"
|
|
tar -tzf "$temporary" >/dev/null
|
|
chmod 0600 "$temporary"
|
|
mv -f "$temporary" "$BACKUP_ARCHIVE"
|
|
}
|
|
|
|
remove_systemd_units() {
|
|
local unit
|
|
rm -f \
|
|
/etc/systemd/system/kaidi-finance.service \
|
|
/etc/systemd/system/kaidi-update.service \
|
|
/etc/systemd/system/kaidi-update.path \
|
|
/etc/systemd/system/multi-user.target.wants/kaidi-finance.service \
|
|
/etc/systemd/system/multi-user.target.wants/kaidi-update.path
|
|
systemd_available || return 0
|
|
systemctl daemon-reload
|
|
for unit in kaidi-finance.service kaidi-update.service kaidi-update.path; do
|
|
systemctl reset-failed "$unit" >/dev/null 2>&1 || true
|
|
done
|
|
}
|
|
|
|
remove_managed_paths() {
|
|
rm -rf -- \
|
|
"$APP_ROOT" \
|
|
"$BAOTA_ROOT" \
|
|
"$CONFIG_ROOT" \
|
|
"$STATE_ROOT" \
|
|
"$UPDATE_STATE_ROOT" \
|
|
"$LOG_ROOT"
|
|
rm -f -- "$FIRST_LOGIN_FILE"
|
|
}
|
|
|
|
remove_download_archives() {
|
|
rm -f -- /tmp/kaidi-finance-*.tar.gz
|
|
}
|
|
|
|
remove_service_identity() {
|
|
local entry home shell group_entry gid members primary_users
|
|
entry=$(getent passwd "$SERVICE_USER" 2>/dev/null || true)
|
|
if [ -n "$entry" ]; then
|
|
home=$(printf '%s\n' "$entry" | awk -F: '{print $6}')
|
|
shell=$(printf '%s\n' "$entry" | awk -F: '{print $7}')
|
|
case "$home:$shell" in
|
|
"$STATE_ROOT:"*/nologin|"$STATE_ROOT:"*/false)
|
|
userdel "$SERVICE_USER" \
|
|
|| die "Failed to remove the dedicated $SERVICE_USER service account"
|
|
;;
|
|
*)
|
|
log "Keeping pre-existing user $SERVICE_USER because its home or shell is not Kaidi-managed"
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
group_entry=$(getent group "$SERVICE_GROUP" 2>/dev/null || true)
|
|
[ -n "$group_entry" ] || return 0
|
|
gid=$(printf '%s\n' "$group_entry" | awk -F: '{print $3}')
|
|
members=$(printf '%s\n' "$group_entry" | awk -F: '{print $4}')
|
|
primary_users=$(getent passwd | awk -F: -v gid="$gid" '$4 == gid { print $1 }')
|
|
if [ -z "$members" ] && [ -z "$primary_users" ]; then
|
|
groupdel "$SERVICE_GROUP" \
|
|
|| die "Failed to remove the dedicated $SERVICE_GROUP service group"
|
|
else
|
|
log "Keeping group $SERVICE_GROUP because another account still uses it"
|
|
fi
|
|
}
|
|
|
|
main() {
|
|
validate_inputs
|
|
log "External MySQL data and reverse-proxy configuration will not be modified"
|
|
stop_systemd_units
|
|
stop_managed_processes
|
|
create_recovery_backup
|
|
remove_systemd_units
|
|
remove_managed_paths
|
|
remove_download_archives
|
|
remove_service_identity
|
|
stop_managed_processes
|
|
log "Local Kaidi installation state has been removed"
|
|
if [ -n "$BACKUP_ARCHIVE" ]; then
|
|
log "Recovery backup: $BACKUP_ARCHIVE"
|
|
fi
|
|
log "Use a new empty MySQL database for the next installation"
|
|
}
|
|
|
|
main "$@"
|