From 9d0752b21496bd8e328dc02fb2d0ae499ee305ff Mon Sep 17 00:00:00 2001 From: ochenstarik-ui Date: Tue, 18 Aug 2026 02:53:37 +0700 Subject: [PATCH] feat: add complete server uninstall mode --- deploy/ochenstarik-server-monitor-manager.sh | 231 +++++++++++++++++- deploy/smm-setup.sh | 51 +++- docs/linux-bootstrap.md | 21 +- tests/bootstrap/run-native-systemd-smoke.sh | 39 ++- .../bootstrap/run-systemd-container-smoke.sh | 60 +++++ tests/bootstrap/systemd-container.Dockerfile | 2 +- tests/bootstrap/test-bootstrap-contract.sh | 9 +- tests/bootstrap/test-unified-installer.sh | 21 ++ tests/bootstrap/test-uninstall-mode.sh | 55 +++++ 9 files changed, 470 insertions(+), 19 deletions(-) create mode 100644 tests/bootstrap/test-uninstall-mode.sh diff --git a/deploy/ochenstarik-server-monitor-manager.sh b/deploy/ochenstarik-server-monitor-manager.sh index 989d0c6..216936a 100755 --- a/deploy/ochenstarik-server-monitor-manager.sh +++ b/deploy/ochenstarik-server-monitor-manager.sh @@ -11,13 +11,18 @@ readonly ENROLLMENT_DIR="${STATE_DIR}-enrollment" readonly BACKUP_DIR="${STATE_DIR}/bootstrap-backups" readonly CONTROL_USER="ochenstarik-smm-control" readonly AGENT_USER="ochenstarik-smm-agent" +readonly MONITOR_USER="ochenstarik-monitor" +readonly MONITOR_HOME="/var/lib/ochenstarik-monitor" readonly CONTROL_UNIT="ochenstarik-smm-control.service" readonly AGENT_UNIT="ochenstarik-smm-agent.service" readonly PROVISIONING_HELPER_UNIT="ochenstarik-smm-provisioning-helper.service" readonly POLICY_HELPER="/usr/local/libexec/ochenstarik-smm-policy-apply" readonly EMERGENCY_COMMAND="/usr/local/sbin/ochenstarik-smm-emergency" readonly BOOTSTRAP_COMMAND="/usr/local/sbin/ochenstarik-server-monitor-manager.sh" +readonly METRICS_SCRIPT="/usr/local/libexec/ochenstarik-smm-metrics" readonly SUDOERS_FILE="/etc/sudoers.d/ochenstarik-smm-control" +readonly SYSCTL_FILE="/etc/sysctl.d/90-ochenstarik-smm-mesh.conf" +readonly SYSTEM_WG_CONFIG="/etc/wireguard/smm0.conf" readonly MESH_DIR="${STATE_DIR}/mesh" readonly WG_DIR="${ETC_DIR}/wireguard" readonly FIREWALL_UNIT="ochenstarik-smm-firewall.service" @@ -91,6 +96,8 @@ Usage: ochenstarik-server-monitor-manager.sh status ochenstarik-server-monitor-manager.sh uninstall-agent [--purge] ochenstarik-server-monitor-manager.sh uninstall-control --confirm-destroy-control + ochenstarik-server-monitor-manager.sh uninstall-system-plan [--purge-data] + ochenstarik-server-monitor-manager.sh uninstall-system --confirm-uninstall [--purge-data --confirm-destroy-data] ochenstarik-server-monitor-manager.sh version ARCHIVE must have a matching ARCHIVE.sha256 file. Agent enrollment reads the @@ -1438,10 +1445,9 @@ install_monitor() { local public_key="$1" require_root validate_platform - - local metrics_script="/usr/local/libexec/ochenstarik-smm-metrics" - local monitor_user="ochenstarik-monitor" - local monitor_home="/var/lib/ochenstarik-monitor" + local metrics_script="$METRICS_SCRIPT" + local monitor_user="$MONITOR_USER" + local monitor_home="$MONITOR_HOME" if ! id -u "$monitor_user" >/dev/null 2>&1; then useradd -r -s /usr/sbin/nologin -d "$monitor_home" -M "$monitor_user" @@ -1516,9 +1522,9 @@ EOF uninstall_monitor() { require_root - local monitor_user="ochenstarik-monitor" - local monitor_home="/var/lib/ochenstarik-monitor" - local metrics_script="/usr/local/libexec/ochenstarik-smm-metrics" + local monitor_user="$MONITOR_USER" + local monitor_home="$MONITOR_HOME" + local metrics_script="$METRICS_SCRIPT" if id -u "$monitor_user" >/dev/null 2>&1; then userdel -f "$monitor_user" || true @@ -1543,6 +1549,215 @@ uninstall_control() { log "Control role and its state were removed." } +owned_unit_exists() { + local unit="$1" + [[ -e "/etc/systemd/system/$unit" || -L "/etc/systemd/system/$unit" ]] && return 0 + systemctl list-unit-files "$unit" --no-legend 2>/dev/null | grep -q "^${unit}[[:space:]]" +} + +path_has_content() { + local path="$1" + [[ -f "$path" || -L "$path" ]] && return 0 + [[ -d "$path" ]] || return 1 + find "$path" -mindepth 1 -maxdepth 1 -print -quit 2>/dev/null | grep -q . +} + +uninstall_system_plan() { + local purge_data="${1:-}" found=0 unit user path + local -a units=( + "$CONTROL_UNIT" "$AGENT_UNIT" "$PROVISIONING_HELPER_UNIT" + "$FIREWALL_UNIT" wg-quick@smm0.service + ) + local -a users=("$CONTROL_USER" "$AGENT_USER" "$MONITOR_USER") + local -a files=( + "$POLICY_HELPER" "$METRICS_SCRIPT" "$EMERGENCY_COMMAND" "$BOOTSTRAP_COMMAND" + "$SUDOERS_FILE" "$SYSCTL_FILE" "$SYSTEM_WG_CONFIG" + ) + local -a program_paths=("$LIB_DIR" "$MONITOR_HOME" "$ENROLLMENT_DIR") + printf '%s\n' 'Server Monitor Manager objects found on this machine:' + for unit in "${units[@]}"; do + if owned_unit_exists "$unit"; then + printf ' unit: %s\n' "$unit" + found=1 + fi + done + for user in "${users[@]}"; do + if id -u "$user" >/dev/null 2>&1; then + printf ' user/group: %s\n' "$user" + found=1 + fi + done + if command -v nft >/dev/null 2>&1 \ + && nft list table inet ochenstarik_smm >/dev/null 2>&1; then + printf '%s\n' ' nftables table: inet ochenstarik_smm' + found=1 + fi + if command -v ip >/dev/null 2>&1 && ip link show smm0 >/dev/null 2>&1; then + printf '%s\n' ' network interface: smm0' + found=1 + fi + for path in "${files[@]}" "${program_paths[@]}"; do + if path_has_content "$path"; then + printf ' path: %s\n' "$path" + found=1 + fi + done + for path in "$ETC_DIR" "$STATE_DIR"; do + if path_has_content "$path"; then + if [[ "$purge_data" == "--purge-data" ]]; then + printf ' data path: %s\n' "$path" + else + printf ' program files under: %s (Control database, backups, and CA are preserved)\n' "$path" + fi + found=1 + fi + done + if [[ "$found" == "0" ]]; then + printf '%s\n' ' nothing installed' + fi +} + +stop_owned_services() { + local unit + local -a units=( + "$CONTROL_UNIT" "$AGENT_UNIT" "$PROVISIONING_HELPER_UNIT" + "$FIREWALL_UNIT" wg-quick@smm0.service + ) + for unit in "${units[@]}"; do + systemctl disable --now "$unit" >/dev/null 2>&1 || true + done +} + +terminate_owned_processes() { + local user running=0 + local -a users=("$CONTROL_USER" "$AGENT_USER" "$MONITOR_USER") + for user in "${users[@]}"; do + if id -u "$user" >/dev/null 2>&1 && pgrep -u "$user" >/dev/null 2>&1; then + pkill -TERM -u "$user" || true + running=1 + fi + done + [[ "$running" == "0" ]] || sleep 1 + for user in "${users[@]}"; do + if id -u "$user" >/dev/null 2>&1 && pgrep -u "$user" >/dev/null 2>&1; then + pkill -KILL -u "$user" || true + fi + done + for user in "${users[@]}"; do + if id -u "$user" >/dev/null 2>&1 && pgrep -u "$user" >/dev/null 2>&1; then + fail "Processes owned by $user are still running." + fi + done +} + +remove_owned_accounts() { + local user + local -a users=("$CONTROL_USER" "$AGENT_USER" "$MONITOR_USER") + for user in "${users[@]}"; do + if id -u "$user" >/dev/null 2>&1; then + userdel "$user" + fi + done + for user in "${users[@]}"; do + if getent group "$user" >/dev/null 2>&1; then + groupdel "$user" || log "Group $user remains because it is still in use." + fi + done +} + +remove_owned_network() { + if command -v ip >/dev/null 2>&1 && ip link show smm0 >/dev/null 2>&1; then + ip link delete smm0 + fi + if command -v nft >/dev/null 2>&1 \ + && nft list table inet ochenstarik_smm >/dev/null 2>&1; then + nft delete table inet ochenstarik_smm + fi +} + +remove_owned_files() { + local purge_data="$1" + rm -f -- \ + "/etc/systemd/system/$CONTROL_UNIT" \ + "/etc/systemd/system/$AGENT_UNIT" \ + "/etc/systemd/system/$PROVISIONING_HELPER_UNIT" \ + "/etc/systemd/system/$FIREWALL_UNIT" \ + "$POLICY_HELPER" "$METRICS_SCRIPT" "$EMERGENCY_COMMAND" \ + "$SUDOERS_FILE" "$SYSCTL_FILE" "$SYSTEM_WG_CONFIG" + rm -rf -- "$LIB_DIR" "$MONITOR_HOME" "$ENROLLMENT_DIR" + if [[ "$purge_data" == "--purge-data" ]]; then + rm -rf -- "$ETC_DIR" "$STATE_DIR" + else + rm -f -- \ + "$ETC_DIR/agent.env" "$ETC_DIR/control.env" \ + "$ETC_DIR/control-public-url" "$ETC_DIR/control-server.pfx" \ + "$ETC_DIR/mesh.env" "$ETC_DIR/mesh.nft" \ + "$ETC_DIR/installed-version-agent" "$ETC_DIR/installed-version-control" + rm -rf -- "$WG_DIR" "$STATE_DIR/agent" "$STATE_DIR/mesh" "$STATE_DIR/provisioning" + fi + rm -f -- "$BOOTSTRAP_COMMAND" + rmdir "$ETC_DIR" "$STATE_DIR" 2>/dev/null || true + systemctl daemon-reload + systemctl reset-failed >/dev/null 2>&1 || true +} + +port_state() { + local port="$1" + if command -v ss >/dev/null 2>&1 \ + && ss -H -lntu "sport = :$port" 2>/dev/null | grep -q .; then + printf '%s' 'in-use' + else + printf '%s' 'free' + fi +} + +show_uninstall_result() { + local unit user units=() users=() + printf 'port-7443: %s\n' "$(port_state 7443)" + printf 'port-51820: %s\n' "$(port_state 51820)" + if command -v ip >/dev/null 2>&1 \ + && ip route show 2>/dev/null | grep -Fq '10.77.0.'; then + printf '%s\n' 'mesh-routes: present' + else + printf '%s\n' 'mesh-routes: absent' + fi + while IFS= read -r unit; do + [[ -n "$unit" ]] && units+=("${unit%%[[:space:]]*}") + done < <(systemctl list-unit-files 'ochenstarik-smm-*' --no-legend 2>/dev/null || true) + if (( ${#units[@]} == 0 )); then + printf '%s\n' 'owned-units: absent' + else + printf 'owned-units: present (%s)\n' "${units[*]}" + fi + for user in "$CONTROL_USER" "$AGENT_USER" "$MONITOR_USER"; do + id -u "$user" >/dev/null 2>&1 && users+=("$user") + done + if (( ${#users[@]} == 0 )); then + printf '%s\n' 'owned-users: absent' + else + printf 'owned-users: present (%s)\n' "${users[*]}" + fi +} + +uninstall_system() { + local confirmation="${1:-}" purge_data="${2:-}" data_confirmation="${3:-}" + [[ "$confirmation" == "--confirm-uninstall" ]] \ + || fail "System removal requires --confirm-uninstall" + if [[ -n "$purge_data" ]]; then + [[ "$purge_data" == "--purge-data" && "$data_confirmation" == "--confirm-destroy-data" ]] \ + || fail "Permanent data removal requires --purge-data --confirm-destroy-data" + fi + require_root + uninstall_system_plan "$purge_data" + stop_owned_services + terminate_owned_processes + remove_owned_network + remove_owned_files "$purge_data" + remove_owned_accounts + show_uninstall_result + log "System removal completed${purge_data:+, including Control data and CA}." +} + preflight() { validate_platform local command_name @@ -1580,6 +1795,8 @@ main() { status) [[ $# -eq 0 ]] || fail "status takes no arguments"; show_status ;; uninstall-agent) [[ $# -eq 0 || ( $# -eq 1 && "$1" == "--purge" ) ]] || fail "uninstall-agent accepts only [--purge]"; uninstall_agent "${1:-}" ;; uninstall-control) [[ $# -eq 1 ]] || fail "uninstall-control requires confirmation"; uninstall_control "$1" ;; + uninstall-system-plan) [[ $# -eq 0 || ( $# -eq 1 && "$1" == "--purge-data" ) ]] || fail "uninstall-system-plan accepts only [--purge-data]"; uninstall_system_plan "${1:-}" ;; + uninstall-system) [[ $# -eq 1 || $# -eq 3 ]] || fail "uninstall-system requires --confirm-uninstall [--purge-data --confirm-destroy-data]"; uninstall_system "$@" ;; *) fail "Unknown action: $action (run with --help)" ;; esac } diff --git a/deploy/smm-setup.sh b/deploy/smm-setup.sh index ec6c481..66a1042 100644 --- a/deploy/smm-setup.sh +++ b/deploy/smm-setup.sh @@ -23,9 +23,10 @@ Usage: smm-setup.sh [--tag TAG] [--repository OWNER/REPO] COMMAND [ARG...] With no arguments, an interactive terminal guides a complete Hub or Node -installation. Non-interactive commands remain available: +installation or a full removal. Non-interactive commands remain available: install-hub PUBLIC_HOST [HTTPS_PORT] [WG_PORT] install-node + uninstall-system --confirm-uninstall [--purge-data --confirm-destroy-data] Other commands are passed to the verified ochenstarik-server-monitor-manager.sh asset. Use -- before a command to force pass-through. Common commands: @@ -45,6 +46,7 @@ smm-setup: interactive installation requires a terminal on stdin and stdout. For automation, choose an explicit command: sudo ./smm-setup.sh install-hub PUBLIC_HOST [HTTPS_PORT] [WG_PORT] sudo ./smm-setup.sh install-node + sudo ./smm-setup.sh uninstall-system --confirm-uninstall Run ./smm-setup.sh --help for pass-through commands. HELP } @@ -168,7 +170,7 @@ inspect_node_code() { choose_interactive_action() { local role host https_port wg_port existing default_host show_machine - read -r -p 'Choose role: 1) Hub 2) Node: ' role + read -r -p 'Choose role: 1) Hub 2) Node 3) Uninstall: ' role case "${role,,}" in 1|hub) action=install-hub; existing="$(role_status Hub)" @@ -186,7 +188,12 @@ choose_interactive_action() { [[ -n "$node_code" ]] || die "SMMNODE2 code is empty" action_args=() ;; - *) die "choose Hub (1) or Node (2)" ;; + 3|uninstall|remove) + action=uninstall-system + action_args=() + return + ;; + *) die "choose Hub (1), Node (2), or Uninstall (3)" ;; esac if [[ "$existing" == "installed" ]] && ! confirm "This role is already installed. Reinstall or update it?"; then printf 'No changes were made.\n'; exit 0 @@ -201,6 +208,31 @@ choose_interactive_action() { fi } +run_interactive_uninstall() { + local depth answer + local -a plan_args=() + printf '\nRemoval depth:\n' + printf '%s\n' ' 1) Remove programs and accounts; preserve Control database, backups, and CA' + printf '%s\n' ' 2) Permanently remove everything, including Control database, backups, and CA' + read -r -p 'Choose removal depth [1]: ' depth + case "${depth:-1}" in + 1|preserve) ;; + 2|purge) plan_args=(--purge-data) ;; + *) die "choose removal depth 1 or 2" ;; + esac + + printf '\n' + "$cached_script" uninstall-system-plan "${plan_args[@]}" + read -r -p 'Type UNINSTALL to remove the objects listed above: ' answer + [[ "$answer" == "UNINSTALL" ]] || die "removal cancelled: confirmation word did not match" + if (( ${#plan_args[@]} > 0 )); then + read -r -p 'Type DESTROY-DATA to permanently delete Control data, backups, and CA: ' answer + [[ "$answer" == "DESTROY-DATA" ]] || die "removal cancelled: data-destruction confirmation did not match" + exec "$cached_script" uninstall-system --confirm-uninstall --purge-data --confirm-destroy-data + fi + exec "$cached_script" uninstall-system --confirm-uninstall +} + original_count=$# pass_through=0 while [[ $# -gt 0 ]]; do @@ -232,6 +264,15 @@ if (( pass_through == 0 )); then case "$action" in install-hub) [[ ${#action_args[@]} -ge 1 && ${#action_args[@]} -le 3 ]] || die "install-hub requires PUBLIC_HOST [HTTPS_PORT] [WG_PORT]" ;; install-node) [[ ${#action_args[@]} -eq 0 ]] || die "install-node takes no arguments" ;; + uninstall-system) + if (( interactive == 0 )); then + [[ ( ${#action_args[@]} -eq 1 && "${action_args[0]}" == "--confirm-uninstall" ) \ + || ( ${#action_args[@]} -eq 3 && "${action_args[0]}" == "--confirm-uninstall" \ + && "${action_args[1]}" == "--purge-data" \ + && "${action_args[2]}" == "--confirm-destroy-data" ) ]] \ + || die "uninstall-system requires --confirm-uninstall [--purge-data --confirm-destroy-data]" + fi + ;; esac fi @@ -311,5 +352,9 @@ case "$action" in exec "$cached_script" install-node "$archive" fi ;; + uninstall-system) + if (( interactive == 1 )); then run_interactive_uninstall; fi + exec "$cached_script" "$action" "${action_args[@]}" + ;; *) exec "$cached_script" "$action" "${action_args[@]}" ;; esac diff --git a/docs/linux-bootstrap.md b/docs/linux-bootstrap.md index d9dbf4d..4c640d7 100644 --- a/docs/linux-bootstrap.md +++ b/docs/linux-bootstrap.md @@ -21,7 +21,7 @@ chmod 700 smm-setup.sh sudo ./smm-setup.sh ``` -Интерактивный режим показывает дистрибутив, архитектуру, определённый публичный адрес и уже установленные роли, затем предлагает выбрать Hub или Node. Для Hub он предлагает публичный адрес и порты HTTPS `7443`/WireGuard `51820`. Для Node он принимает готовый `SMMNODE2`, показывает извлечённые адрес Hub и SHA-256 fingerprint CA и продолжает только после подтверждения оператора. Загрузки сопровождаются индикатором; системные зависимости и закреплённый `cosign` устанавливаются и проверяются до использования. +Интерактивный режим показывает дистрибутив, архитектуру, определённый публичный адрес и уже установленные роли, затем предлагает выбрать Hub, Node или полное удаление. Для Hub он предлагает публичный адрес и порты HTTPS `7443`/WireGuard `51820`. Для Node он принимает готовый `SMMNODE2`, показывает извлечённые адрес Hub и SHA-256 fingerprint CA и продолжает только после подтверждения оператора. Загрузки сопровождаются индикатором; системные зависимости и закреплённый `cosign` устанавливаются и проверяются до использования. После установки Hub установщик печатает код регистрации устройства `SMMDEV1-...` и fingerprint CA — оба значения предназначены для подключения приложения оператора. После установки Node он печатает `SMMPEER1...`; вставьте этот код в карточку соответствующего Node в приложении оператора. @@ -146,6 +146,25 @@ sudo ./ochenstarik-server-monitor-manager.sh uninstall-agent --purge sudo ./ochenstarik-server-monitor-manager.sh uninstall-control --confirm-destroy-control ``` +## Полное удаление с сервера + +Запустите `sudo ./smm-setup.sh`, выберите `3) Uninstall`, затем глубину очистки. Перед изменениями установщик показывает найденные units, пользователей, сетевые объекты и каталоги проекта. Удаление начинается только после ввода слова `UNINSTALL`, а необратимое удаление данных — после дополнительного ввода `DESTROY-DATA`. + +Обычная глубина останавливает сервисы, завершает процессы системных пользователей проекта и удаляет программы, accounts, интерфейс `smm0` и только nftables-таблицу `inet ochenstarik_smm`. При этом сохраняются база Control, резервные копии bootstrap и локальный Control CA. Полная глубина удаляет также эти данные без возможности восстановления. Объекты `3x-ui` и другие посторонние users, units, интерфейсы, таблицы и файлы не затрагиваются. + +Для автоматизации интерактивных вопросов нет; обязательные флаги являются явным подтверждением: + +```bash +# Удалить программу, сохранив базу Control, backups и CA +sudo ./smm-setup.sh uninstall-system --confirm-uninstall + +# Необратимо удалить также базу Control, backups и CA +sudo ./smm-setup.sh uninstall-system --confirm-uninstall \ + --purge-data --confirm-destroy-data +``` + +Команда без `--confirm-uninstall`, а полная очистка без `--confirm-destroy-data`, завершается отказом до удаления. Повторный запуск на уже очищенной машине безопасен и возвращает успешный результат. В конце выводится состояние портов `7443` и `51820`, маршрутов `10.77.0.*`, units и пользователей проекта. + Update создаёт root-only backup перед заменой binaries и автоматически восстанавливает предыдущую версию, если сервис не запускается. Перед alpha-тестом на реальных серверах обязательно сохраните отдельную консольную/SSH-сессию и не закрывайте основной административный доступ firewall-правилами проекта. ## Локальное аварийное восстановление diff --git a/tests/bootstrap/run-native-systemd-smoke.sh b/tests/bootstrap/run-native-systemd-smoke.sh index bdfb8d2..31b15f6 100755 --- a/tests/bootstrap/run-native-systemd-smoke.sh +++ b/tests/bootstrap/run-native-systemd-smoke.sh @@ -2,11 +2,13 @@ set -Eeuo pipefail IFS=$'\n\t' -archive="${1:?usage: run-native-systemd-smoke.sh ARCHIVE BOOTSTRAP}" -bootstrap="${2:?usage: run-native-systemd-smoke.sh ARCHIVE BOOTSTRAP}" +archive="$(realpath "${1:?usage: run-native-systemd-smoke.sh ARCHIVE BOOTSTRAP}")" +bootstrap="$(realpath "${2:?usage: run-native-systemd-smoke.sh ARCHIVE BOOTSTRAP}")" port="${SMM_SMOKE_PORT:-17443}" system_bootstrap="/usr/local/sbin/ochenstarik-server-monitor-manager.sh" probe_dir="" +foreign_user="smm-uninstall-foreign" +foreign_unit="smm-uninstall-foreign.service" # If manifest+sig are not shipped alongside the archive (CI-only builds), # allow unsigned verification via .sha256 fallback. @@ -19,8 +21,11 @@ cleanup() { if [[ -n "$probe_dir" ]]; then rm -rf -- "$probe_dir" fi - sudo "$system_bootstrap" uninstall-agent --purge >/dev/null 2>&1 || true - sudo "$system_bootstrap" uninstall-control --confirm-destroy-control >/dev/null 2>&1 || true + sudo "$bootstrap" uninstall-system --confirm-uninstall --purge-data \ + --confirm-destroy-data >/dev/null 2>&1 || true + sudo rm -f -- "/etc/systemd/system/$foreign_unit" + sudo userdel "$foreign_user" >/dev/null 2>&1 || true + sudo systemctl daemon-reload >/dev/null 2>&1 || true } trap cleanup EXIT @@ -116,4 +121,30 @@ sudo curl --fail --silent --show-error --retry 15 --retry-all-errors --retry-del --cacert /etc/ochenstarik-server-monitor-manager/control-ca.crt \ "https://127.0.0.1:$port/healthz" +sudo install -d -m 0700 /var/lib/ochenstarik-server-monitor-manager/bootstrap-backups +sudo touch /var/lib/ochenstarik-server-monitor-manager/bootstrap-backups/smoke-preserve +sudo useradd --system --no-create-home "$foreign_user" +printf '[Unit]\nDescription=Foreign smoke unit\n' | sudo tee "/etc/systemd/system/$foreign_unit" >/dev/null +sudo "$system_bootstrap" uninstall-system --confirm-uninstall +sudo test -s /var/lib/ochenstarik-server-monitor-manager/control/control.db +sudo test -f /var/lib/ochenstarik-server-monitor-manager/bootstrap-backups/smoke-preserve +sudo test -f /etc/ochenstarik-server-monitor-manager/control-ca.pfx +sudo test -f /etc/ochenstarik-server-monitor-manager/control-ca.crt +sudo id "$foreign_user" +sudo test -f "/etc/systemd/system/$foreign_unit" +if sudo ss -H -ltn "sport = :$port" | grep -q .; then + printf 'Control port survived uninstall: %s\n' "$port" >&2 + exit 1 +fi + +sudo "$bootstrap" uninstall-system --confirm-uninstall --purge-data --confirm-destroy-data +sudo test ! -e /etc/ochenstarik-server-monitor-manager +sudo test ! -e /var/lib/ochenstarik-server-monitor-manager +sudo test ! -e /var/lib/ochenstarik-server-monitor-manager-enrollment +sudo id "$foreign_user" +sudo test -f "/etc/systemd/system/$foreign_unit" +empty_uninstall_output="$(sudo "$bootstrap" uninstall-system --confirm-uninstall \ + --purge-data --confirm-destroy-data)" +grep -Fq 'nothing installed' <<<"$empty_uninstall_output" + printf '%s\n' "NATIVE_SYSTEMD_SMOKE=PASS" diff --git a/tests/bootstrap/run-systemd-container-smoke.sh b/tests/bootstrap/run-systemd-container-smoke.sh index 1155437..d50baa7 100755 --- a/tests/bootstrap/run-systemd-container-smoke.sh +++ b/tests/bootstrap/run-systemd-container-smoke.sh @@ -12,6 +12,7 @@ port="17443" smoke_dir="/root/smm-smoke" remote_archive="$smoke_dir/release.tar.gz" remote_bootstrap="$smoke_dir/ochenstarik-server-monitor-manager.sh" +system_bootstrap="/usr/local/sbin/ochenstarik-server-monitor-manager.sh" cleanup() { docker rm -f "$name" >/dev/null 2>&1 || true @@ -78,4 +79,63 @@ docker exec "$name" curl --fail --silent --show-error --retry 15 --retry-all-err "https://127.0.0.1:$port/healthz" docker exec "$name" /usr/local/sbin/ochenstarik-smm-emergency status +# Exercise both removal depths with live services, owned network objects, and +# similarly placed foreign objects that must survive. +docker exec "$name" install -d -m 0700 /var/lib/ochenstarik-server-monitor-manager/bootstrap-backups +docker exec "$name" touch /var/lib/ochenstarik-server-monitor-manager/bootstrap-backups/smoke-preserve +docker exec "$name" useradd --system --user-group --no-create-home ochenstarik-smm-agent +docker exec "$name" useradd --system --user-group --no-create-home ochenstarik-monitor +docker exec "$name" install -d -m 0700 -o ochenstarik-monitor -g ochenstarik-monitor /var/lib/ochenstarik-monitor +docker exec "$name" useradd --system --no-create-home smm-uninstall-foreign +docker exec "$name" sh -c "printf '[Unit]\nDescription=Foreign smoke unit\n' >/etc/systemd/system/smm-uninstall-foreign.service" +docker exec "$name" ip link add smm0 type dummy +docker exec "$name" nft add table inet ochenstarik_smm + +docker exec "$name" "$system_bootstrap" uninstall-system --confirm-uninstall +docker exec "$name" test -s /var/lib/ochenstarik-server-monitor-manager/control/control.db +docker exec "$name" test -f /var/lib/ochenstarik-server-monitor-manager/bootstrap-backups/smoke-preserve +docker exec "$name" test -f /etc/ochenstarik-server-monitor-manager/control-ca.pfx +docker exec "$name" test -f /etc/ochenstarik-server-monitor-manager/control-ca.crt +docker exec "$name" id smm-uninstall-foreign +docker exec "$name" test -f /etc/systemd/system/smm-uninstall-foreign.service +docker exec "$name" sh -c "! ss -H -ltn 'sport = :$port' | grep -q ." +for owned_user in ochenstarik-smm-control ochenstarik-smm-agent ochenstarik-monitor; do + if docker exec "$name" id "$owned_user" >/dev/null 2>&1; then + printf 'owned user survived uninstall: %s\n' "$owned_user" >&2 + exit 1 + fi +done +for owned_unit in ochenstarik-smm-control.service ochenstarik-smm-agent.service \ + ochenstarik-smm-provisioning-helper.service ochenstarik-smm-firewall.service; do + if docker exec "$name" test -e "/etc/systemd/system/$owned_unit"; then + printf 'owned unit survived uninstall: %s\n' "$owned_unit" >&2 + exit 1 + fi +done +if docker exec "$name" ip link show smm0 >/dev/null 2>&1; then + printf '%s\n' 'owned smm0 interface survived uninstall' >&2 + exit 1 +fi +if docker exec "$name" nft list table inet ochenstarik_smm >/dev/null 2>&1; then + printf '%s\n' 'owned nftables table survived uninstall' >&2 + exit 1 +fi + +docker exec "$name" "$remote_bootstrap" uninstall-system --confirm-uninstall \ + --purge-data --confirm-destroy-data +for owned_path in /etc/ochenstarik-server-monitor-manager \ + /var/lib/ochenstarik-server-monitor-manager \ + /var/lib/ochenstarik-server-monitor-manager-enrollment \ + /var/lib/ochenstarik-monitor; do + if docker exec "$name" test -e "$owned_path"; then + printf 'owned path survived purge: %s\n' "$owned_path" >&2 + exit 1 + fi +done +docker exec "$name" id smm-uninstall-foreign +docker exec "$name" test -f /etc/systemd/system/smm-uninstall-foreign.service +empty_uninstall_output="$(docker exec "$name" "$remote_bootstrap" uninstall-system \ + --confirm-uninstall --purge-data --confirm-destroy-data)" +grep -Fq 'nothing installed' <<<"$empty_uninstall_output" + printf '%s\n' "SYSTEMD_CONTAINER_SMOKE=PASS image=$base_image" diff --git a/tests/bootstrap/systemd-container.Dockerfile b/tests/bootstrap/systemd-container.Dockerfile index 3790429..fe40094 100644 --- a/tests/bootstrap/systemd-container.Dockerfile +++ b/tests/bootstrap/systemd-container.Dockerfile @@ -6,7 +6,7 @@ ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update \ && apt-get install -y --no-install-recommends \ - ca-certificates curl iproute2 openssl procps sudo systemd systemd-sysv \ + ca-certificates curl iproute2 nftables openssl procps sudo systemd systemd-sysv \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* diff --git a/tests/bootstrap/test-bootstrap-contract.sh b/tests/bootstrap/test-bootstrap-contract.sh index 23b5882..7e3bced 100755 --- a/tests/bootstrap/test-bootstrap-contract.sh +++ b/tests/bootstrap/test-bootstrap-contract.sh @@ -9,13 +9,15 @@ emergency="$root/deploy/ochenstarik-smm-emergency" acceptance="$root/tests/acceptance/three-server-mesh.sh" unified_setup="$root/deploy/smm-setup.sh" unified_test="$root/tests/bootstrap/test-unified-installer.sh" +uninstall_test="$root/tests/bootstrap/test-uninstall-mode.sh" -bash -n "$unified_setup" "$unified_test" +bash -n "$unified_setup" "$unified_test" "$uninstall_test" if command -v shellcheck >/dev/null 2>&1; then - shellcheck --severity=error "$unified_setup" "$unified_test" + shellcheck --severity=error "$unified_setup" "$unified_test" "$uninstall_test" fi if [[ "$(uname -s)" != MINGW* ]]; then bash "$unified_test" + bash "$uninstall_test" fi grep -Fq 'listing="$(/usr/sbin/nft -a list chain' "$helper" || { @@ -147,7 +149,8 @@ if ! bash "$bootstrap" >/dev/null 2>&1; then exit 1 fi for action in verify-release install-control install-agent install-node mesh-init peer-add \ - update-control update-agent rollback node-code control-device-code node-token uninstall-control; do + update-control update-agent rollback node-code control-device-code node-token uninstall-control \ + uninstall-system; do if bash "$bootstrap" "$action" >/dev/null 2>&1; then printf 'bootstrap action accepted missing arguments: %s\n' "$action" >&2 exit 1 diff --git a/tests/bootstrap/test-unified-installer.sh b/tests/bootstrap/test-unified-installer.sh index 6a5015c..4c6d67a 100644 --- a/tests/bootstrap/test-unified-installer.sh +++ b/tests/bootstrap/test-unified-installer.sh @@ -20,6 +20,12 @@ case "$1" in [[ -n "${SMM_ENROLL_CODE:-}" && "${SMM_ACCEPT_CA_FINGERPRINT:-}" == 1 ]] printf '%s\n' 'SMMPEER1.test-node.test-address.test-key' ;; + uninstall-system-plan) + printf '%s\n' 'Server Monitor Manager objects found on this machine:' + printf '%s\n' ' unit: ochenstarik-smm-control.service' + [[ "${2:-}" != "--purge-data" ]] || printf '%s\n' ' data path: /var/lib/ochenstarik-server-monitor-manager' + ;; + uninstall-system) printf 'FAKE_UNINSTALL=%s\n' "$*" ;; *) printf 'PASSTHROUGH=%s\n' "$*" ;; esac INNER @@ -101,6 +107,21 @@ grep -Fq 'CA SHA-256:' "$fixture/node.out" grep -Fq 'SMMPEER1.test-node.test-address.test-key' "$fixture/node.out" grep -Fq 'operator application' "$fixture/node.out" +run_tty $'3\n1\nUNINSTALL\n' "$fixture/uninstall-preserve.out" +grep -Fq 'unit: ochenstarik-smm-control.service' "$fixture/uninstall-preserve.out" +grep -Fq 'FAKE_UNINSTALL=uninstall-system --confirm-uninstall' "$fixture/uninstall-preserve.out" + +run_tty $'3\n2\nUNINSTALL\nDESTROY-DATA\n' "$fixture/uninstall-purge.out" +grep -Fq 'data path: /var/lib/ochenstarik-server-monitor-manager' "$fixture/uninstall-purge.out" +grep -Fq 'FAKE_UNINSTALL=uninstall-system --confirm-uninstall --purge-data --confirm-destroy-data' "$fixture/uninstall-purge.out" + +if PATH="$fixture/bin:$PATH" FIXTURE_RELEASE="$fixture/release" SMM_CACHE_DIR="$fixture/cache" \ + bash "$setup" uninstall-system >"$fixture/uninstall-refused.out" 2>&1; then + printf '%s\n' 'uninstall without confirmation flag unexpectedly succeeded' >&2 + exit 1 +fi +grep -Fq 'requires --confirm-uninstall' "$fixture/uninstall-refused.out" + tampered_ca_part="$(printf '%s' 'not-a-certificate' | b64url)" tampered_code="SMMNODE2.$control_part.$tampered_ca_part.$node_part.$token_part.$endpoint_part.$hub_key_part.$address_part.$network_part" for bad_code in "${valid_code%.*}" "$valid_code.extra" "$tampered_code"; do diff --git a/tests/bootstrap/test-uninstall-mode.sh b/tests/bootstrap/test-uninstall-mode.sh new file mode 100644 index 0000000..daf415b --- /dev/null +++ b/tests/bootstrap/test-uninstall-mode.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +set -Eeuo pipefail +IFS=$'\n\t' + +root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)" +bootstrap="$root/deploy/ochenstarik-server-monitor-manager.sh" +setup="$root/deploy/smm-setup.sh" +refusal_output="$(mktemp -t smm-uninstall-refused.XXXXXXXX)" +trap 'rm -f -- "$refusal_output"' EXIT + +bash -n "$bootstrap" "$setup" + +if bash "$bootstrap" uninstall-system >/dev/null 2>"$refusal_output"; then + printf '%s\n' 'uninstall-system accepted a request without confirmation' >&2 + exit 1 +fi +grep -Fq -- '--confirm-uninstall' "$refusal_output" + +definition="$(awk ' + /^uninstall_system\(\) \{/ { active=1 } + active { print } + active && /^}$/ { exit } +' "$bootstrap")" +stop_line="$(grep -Fn ' stop_owned_services' <<<"$definition" | cut -d: -f1)" +term_line="$(grep -Fn ' terminate_owned_processes' <<<"$definition" | cut -d: -f1)" +network_line="$(grep -Fn ' remove_owned_network' <<<"$definition" | cut -d: -f1)" +files_line="$(grep -Fn ' remove_owned_files' <<<"$definition" | cut -d: -f1)" +accounts_line="$(grep -Fn ' remove_owned_accounts' <<<"$definition" | cut -d: -f1)" +(( stop_line < term_line && term_line < network_line && network_line < files_line && files_line < accounts_line )) + +grep -Fq 'pkill -TERM -u "$user"' "$bootstrap" +grep -Fq 'pkill -KILL -u "$user"' "$bootstrap" +grep -Fq 'nft delete table inet ochenstarik_smm' "$bootstrap" +grep -Fq 'ip link delete smm0' "$bootstrap" +grep -Fq 'port-7443:' "$bootstrap" +grep -Fq 'port-51820:' "$bootstrap" +grep -Fq 'mesh-routes:' "$bootstrap" +grep -Fq 'owned-units:' "$bootstrap" +grep -Fq 'owned-users:' "$bootstrap" +grep -Fq 'control-ca.pfx' "$bootstrap" +grep -Fq 'bootstrap-backups' "$bootstrap" +grep -Fq 'Type UNINSTALL' "$setup" +grep -Fq 'Type DESTROY-DATA' "$setup" +grep -Fq '3) Uninstall' "$setup" + +if grep -Eiq '3x-ui|x-ui' "$bootstrap" "$setup"; then + printf '%s\n' 'uninstall implementation references protected 3x-ui objects' >&2 + exit 1 +fi +if grep -Eq "rm -rf --? [\"']?/(etc|var|usr)(/|[\"']|$)" "$bootstrap"; then + printf '%s\n' 'uninstall implementation contains a broad recursive deletion' >&2 + exit 1 +fi + +printf '%s\n' 'UNINSTALL_MODE=PASS'