243 lines
8.9 KiB
Bash
Executable File
243 lines
8.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
CONFIG_FILE="${ERP_CONFIG_FILE:-/etc/kaidi-erp/erp.env}"
|
|
INSTALL_ROOT="${ERP_INSTALL_ROOT:-/opt/kaidi-erp}"
|
|
CONFIG_ROOT="${ERP_CONFIG_ROOT:-/etc/kaidi-erp}"
|
|
STATE_ROOT="${ERP_STATE_ROOT:-/var/lib/kaidi-erp}"
|
|
LOG_ROOT="${ERP_LOG_ROOT:-/var/log/kaidi-erp}"
|
|
PURGE_DATABASE=0
|
|
ASSUME_YES=0
|
|
|
|
say() { printf '[ERP Uninstall] %s\n' "$*"; }
|
|
fail() { printf '[ERP Uninstall] ERROR: %s\n' "$*" >&2; exit 1; }
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: uninstall.sh [options]
|
|
--purge-database Drop and recreate the public schema using the configured ERP account
|
|
--yes Confirm destructive removal without an interactive prompt
|
|
--config-file PATH Installer-generated erp.env file
|
|
--install-root PATH Installation root (default: /opt/kaidi-erp)
|
|
--config-root PATH Configuration root (default: /etc/kaidi-erp)
|
|
--state-root PATH State root (default: /var/lib/kaidi-erp)
|
|
--log-root PATH Log root (default: /var/log/kaidi-erp)
|
|
EOF
|
|
}
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--purge-database) PURGE_DATABASE=1; shift ;;
|
|
--yes) ASSUME_YES=1; shift ;;
|
|
--config-file) [[ $# -ge 2 ]] || fail '--config-file requires a value'; CONFIG_FILE="$2"; shift 2 ;;
|
|
--install-root) [[ $# -ge 2 ]] || fail '--install-root requires a value'; INSTALL_ROOT="$2"; shift 2 ;;
|
|
--config-root) [[ $# -ge 2 ]] || fail '--config-root requires a value'; CONFIG_ROOT="$2"; shift 2 ;;
|
|
--state-root) [[ $# -ge 2 ]] || fail '--state-root requires a value'; STATE_ROOT="$2"; shift 2 ;;
|
|
--log-root) [[ $# -ge 2 ]] || fail '--log-root requires a value'; LOG_ROOT="$2"; shift 2 ;;
|
|
-h|--help) usage; exit 0 ;;
|
|
*) fail "unknown option: $1" ;;
|
|
esac
|
|
done
|
|
|
|
require_root() {
|
|
if [[ "${ERP_UNINSTALL_TEST_MODE:-0}" != "1" && "$(id -u)" != "0" ]]; then
|
|
fail 'Linux uninstall requires root'
|
|
fi
|
|
}
|
|
|
|
require_confirmation() {
|
|
[[ "$PURGE_DATABASE" == "1" ]] || fail '--purge-database is required for a clean reinstall'
|
|
[[ "$ASSUME_YES" == "1" ]] || fail '--yes is required to confirm database and file removal'
|
|
}
|
|
|
|
validate_remove_path() {
|
|
local path="$1"
|
|
[[ "$path" == /* && "$path" != "/" && ${#path} -ge 8 ]] \
|
|
|| fail "refusing unsafe removal path: $path"
|
|
[[ "$path" =~ ^/[A-Za-z0-9._/@:+-]+$ ]] \
|
|
|| fail "refusing removal path with unsafe characters: $path"
|
|
|
|
local resolved
|
|
resolved="$(python3 - "$path" <<'PY'
|
|
import os
|
|
import sys
|
|
print(os.path.realpath(sys.argv[1]), end="")
|
|
PY
|
|
)" || fail "unable to resolve removal path: $path"
|
|
if [[ "${ERP_UNINSTALL_TEST_MODE:-0}" != "1" && "$resolved" != "$path" ]]; then
|
|
fail "removal path must be canonical and cannot traverse links: $path"
|
|
fi
|
|
|
|
case "$resolved" in
|
|
/bin|/boot|/dev|/etc|/home|/lib|/lib64|/opt|/proc|/root|/run|/sbin|/srv|/sys|/tmp|/usr|/var)
|
|
fail "refusing unsafe removal path: $path"
|
|
;;
|
|
esac
|
|
|
|
if [[ "${ERP_UNINSTALL_TEST_MODE:-0}" != "1" ]]; then
|
|
case "$resolved" in
|
|
/etc/kaidi-erp|/var/lib/kaidi-erp|/var/log/kaidi-erp) ;;
|
|
/bin/*|/boot/*|/dev/*|/etc/*|/lib/*|/lib64/*|/proc/*|/root/*|/run/*|/sbin/*|/sys/*|/tmp/*|/usr/*|/var/*)
|
|
fail "refusing removal below a protected system directory: $path"
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
printf '%s' "$resolved"
|
|
}
|
|
|
|
prepare_paths() {
|
|
command -v python3 >/dev/null 2>&1 || fail 'python3 is required'
|
|
INSTALL_ROOT="$(validate_remove_path "$INSTALL_ROOT")"
|
|
CONFIG_ROOT="$(validate_remove_path "$CONFIG_ROOT")"
|
|
STATE_ROOT="$(validate_remove_path "$STATE_ROOT")"
|
|
LOG_ROOT="$(validate_remove_path "$LOG_ROOT")"
|
|
|
|
local config_resolved
|
|
config_resolved="$(python3 - "$CONFIG_FILE" <<'PY'
|
|
import os
|
|
import sys
|
|
print(os.path.realpath(sys.argv[1]), end="")
|
|
PY
|
|
)" || fail "unable to resolve configuration path: $CONFIG_FILE"
|
|
[[ "$config_resolved" == "$CONFIG_ROOT/erp.env" ]] \
|
|
|| fail "configuration file must be $CONFIG_ROOT/erp.env"
|
|
CONFIG_FILE="$config_resolved"
|
|
}
|
|
|
|
read_config_value() {
|
|
local key="$1"
|
|
python3 - "$CONFIG_FILE" "$key" <<'PY'
|
|
import re
|
|
import shlex
|
|
import sys
|
|
|
|
path, wanted = sys.argv[1:]
|
|
pattern = re.compile(r"[A-Z][A-Z0-9_]*")
|
|
seen = set()
|
|
matches = []
|
|
with open(path, encoding="utf-8") as handle:
|
|
for raw in handle:
|
|
line = raw.rstrip("\r\n")
|
|
if not line or line.startswith("#") or "=" not in line:
|
|
continue
|
|
key, encoded = line.split("=", 1)
|
|
if not pattern.fullmatch(key):
|
|
raise SystemExit("invalid configuration key")
|
|
if key in seen:
|
|
raise SystemExit(f"duplicate configuration key: {key}")
|
|
seen.add(key)
|
|
if key != wanted:
|
|
continue
|
|
values = shlex.split(encoded, posix=True)
|
|
if len(values) != 1:
|
|
raise SystemExit(f"invalid value for {wanted}")
|
|
matches.append(values[0])
|
|
if len(matches) != 1:
|
|
raise SystemExit(f"missing configuration value: {wanted}")
|
|
print(matches[0], end="")
|
|
PY
|
|
}
|
|
|
|
verify_installation_identity() {
|
|
[[ -f "$CONFIG_FILE" && ! -L "$CONFIG_FILE" ]] \
|
|
|| fail "configuration file is missing or unsafe: $CONFIG_FILE"
|
|
|
|
local configured_root configured_lock configured_state
|
|
configured_root="$(read_config_value ERP_INSTALL_ROOT)" \
|
|
|| fail 'unable to verify the configured installation root'
|
|
configured_lock="$(read_config_value ERP_INSTALL_LOCK_FILE)" \
|
|
|| fail 'unable to verify the configured installation state'
|
|
configured_root="$(validate_remove_path "$configured_root")"
|
|
configured_state="$(validate_remove_path "$(dirname "$configured_lock")")"
|
|
[[ "$configured_root" == "$INSTALL_ROOT" ]] \
|
|
|| fail 'requested installation root does not match erp.env'
|
|
[[ "$configured_state" == "$STATE_ROOT" ]] \
|
|
|| fail 'requested state root does not match erp.env'
|
|
}
|
|
|
|
purge_database() {
|
|
command -v psql >/dev/null 2>&1 || fail 'PostgreSQL client psql is required'
|
|
|
|
local host port database sslmode username password owner_check
|
|
host="$(read_config_value ERP_PGHOST)"
|
|
port="$(read_config_value ERP_PGPORT)"
|
|
database="$(read_config_value ERP_PGDATABASE)"
|
|
sslmode="$(read_config_value ERP_PGSSLMODE)"
|
|
username="$(read_config_value OA_DB_USERNAME)"
|
|
password="$(read_config_value OA_DB_PASSWORD)"
|
|
|
|
[[ "$host" != *[[:space:]]* && "$port" =~ ^[0-9]+$ && "$port" -ge 1 && "$port" -le 65535 ]] \
|
|
|| fail 'invalid database endpoint in configuration'
|
|
[[ "$database" =~ ^[A-Za-z_][A-Za-z0-9_-]{0,62}$ ]] || fail 'invalid database name in configuration'
|
|
[[ "$username" =~ ^[A-Za-z_][A-Za-z0-9_.-]{0,127}$ ]] || fail 'invalid database user in configuration'
|
|
case "$database" in postgres|template0|template1) fail "refusing to purge protected database: $database" ;; esac
|
|
case "$sslmode" in disable|allow|prefer|require|verify-ca|verify-full) ;; *) fail 'invalid database SSL mode' ;; esac
|
|
|
|
owner_check="$(PGPASSWORD="$password" PGSSLMODE="$sslmode" psql -XAt \
|
|
-h "$host" -p "$port" -U "$username" -d "$database" -v ON_ERROR_STOP=1 \
|
|
-c "SELECT pg_get_userbyid(datdba) = current_user FROM pg_database WHERE datname = current_database()")" \
|
|
|| fail 'unable to verify database ownership'
|
|
[[ "$owner_check" == "t" ]] \
|
|
|| fail 'configured ERP account is not the database owner; database was not changed'
|
|
|
|
say "Purging PostgreSQL schema $database/public"
|
|
PGPASSWORD="$password" PGSSLMODE="$sslmode" psql -X \
|
|
-h "$host" -p "$port" -U "$username" -d "$database" -v ON_ERROR_STOP=1 <<'SQL'
|
|
DROP SCHEMA IF EXISTS public CASCADE;
|
|
SELECT format('CREATE SCHEMA public AUTHORIZATION %I', current_user) \gexec
|
|
SQL
|
|
}
|
|
|
|
stop_service() {
|
|
if command -v systemctl >/dev/null 2>&1; then
|
|
local load_state
|
|
if ! systemctl stop kaidi-erp.service >/dev/null 2>&1; then
|
|
load_state="$(systemctl show kaidi-erp.service --property=LoadState --value 2>/dev/null || true)"
|
|
[[ "$load_state" == "not-found" ]] \
|
|
|| fail 'unable to stop kaidi-erp.service; database and files were not changed'
|
|
fi
|
|
if systemctl is-active --quiet kaidi-erp.service; then
|
|
fail 'kaidi-erp.service is still active; database and files were not changed'
|
|
fi
|
|
fi
|
|
}
|
|
|
|
remove_service() {
|
|
if command -v systemctl >/dev/null 2>&1; then
|
|
systemctl disable kaidi-erp.service >/dev/null 2>&1 || true
|
|
fi
|
|
local unit_file="${ERP_UNINSTALL_SYSTEMD_UNIT_FILE:-/etc/systemd/system/kaidi-erp.service}"
|
|
if [[ "${ERP_UNINSTALL_TEST_MODE:-0}" != "1" && "$unit_file" != "/etc/systemd/system/kaidi-erp.service" ]]; then
|
|
fail 'custom systemd unit path is only available in test mode'
|
|
fi
|
|
rm -f -- "$unit_file"
|
|
if command -v systemctl >/dev/null 2>&1; then
|
|
systemctl daemon-reload >/dev/null 2>&1 || true
|
|
systemctl reset-failed kaidi-erp.service >/dev/null 2>&1 || true
|
|
fi
|
|
}
|
|
|
|
remove_files() {
|
|
local path
|
|
for path in "$INSTALL_ROOT" "$CONFIG_ROOT" "$STATE_ROOT" "$LOG_ROOT"; do
|
|
[[ ! -e "$path" && ! -L "$path" ]] || rm -rf -- "$path"
|
|
done
|
|
}
|
|
|
|
main() {
|
|
require_root
|
|
require_confirmation
|
|
prepare_paths
|
|
verify_installation_identity
|
|
stop_service
|
|
purge_database
|
|
remove_service
|
|
remove_files
|
|
say 'Kaidi ERP service, files, state, logs, and public database schema were removed'
|
|
}
|
|
|
|
if [[ "${BASH_SOURCE[0]:-$0}" == "$0" ]]; then
|
|
main "$@"
|
|
fi
|