diff --git a/scripts/setup/native_mariadb_port_takeover.sh b/scripts/setup/native_mariadb_port_takeover.sh index 6102773..30e006d 100755 --- a/scripts/setup/native_mariadb_port_takeover.sh +++ b/scripts/setup/native_mariadb_port_takeover.sh @@ -234,10 +234,110 @@ rollback_config_changes() { done } +NATIVE_SERVICE_NAME="" +DID_RESTART_NATIVE=0 +DID_RESTART_DA=0 +# Guards rollback_service_state so it only acts while wired via the EXIT trap +# below. NOTE: this is intentionally an EXIT trap, not an ERR trap - see the +# comment above the trap installation in main() for why. +ROLLBACK_ARMED=0 + +detect_native_service_name() { + NATIVE_SERVICE_NAME="$(mysql_service_name)" + log "Wykryta usługa natywnej MariaDB: $NATIVE_SERVICE_NAME." +} + +restart_native_service() { + systemctl restart "$NATIVE_SERVICE_NAME" || die "Nie udało się zrestartować usługi $NATIVE_SERVICE_NAME." + DID_RESTART_NATIVE=1 +} + +verify_native_listening_new_port() { + local i + for i in $(seq 1 30); do + if port_is_listening "$NEW_NATIVE_PORT" && ! port_is_listening 3306; then + log "Natywna instancja nasłuchuje na porcie $NEW_NATIVE_PORT, port 3306 wolny." + return 0 + fi + sleep 1 + done + die "Natywna instancja nie nasłuchuje na porcie $NEW_NATIVE_PORT po restarcie (lub port 3306 wciąż zajęty)." +} + +restart_directadmin_service() { + systemctl restart directadmin || die "Nie udało się zrestartować usługi directadmin." + DID_RESTART_DA=1 +} + +verify_da_connectivity_new_port() { + local bin + bin="$(mysql_detect_binary mysql)" || die "Nie znaleziono klienta mysql/mariadb do weryfikacji łączności DA." + MYSQL_PWD="$NATIVE_CURRENT_PASS" "$bin" \ + --user="$NATIVE_CURRENT_USER" \ + --protocol=TCP --host=127.0.0.1 --port="$NEW_NATIVE_PORT" \ + -e "SELECT 1" >/dev/null 2>>"$LOG_FILE" \ + || die "DirectAdmin nie może połączyć się z natywną instancją na nowym porcie $NEW_NATIVE_PORT." + log "Potwierdzono łączność DirectAdmina z natywną instancją na porcie $NEW_NATIVE_PORT." +} + +rollback_service_state() { + [ "$ROLLBACK_ARMED" -eq 1 ] || return 0 + rollback_config_changes + restore_custombuild_mysql_management + if [ -n "$NATIVE_SERVICE_NAME" ]; then + systemctl restart "$NATIVE_SERVICE_NAME" \ + || log "OSTRZEŻENIE: nie udało się zrestartować $NATIVE_SERVICE_NAME podczas wycofywania zmian." + fi + if [ "$DID_RESTART_DA" -eq 1 ]; then + systemctl restart directadmin \ + || log "OSTRZEŻENIE: nie udało się zrestartować directadmin podczas wycofywania zmian." + fi + log "Wycofano zmiany przejęcia portu natywnej instancji MariaDB." +} + +main() { + require_root + check_os_guard + read_native_mysql_conf + read_current_mysql_inst + + if already_migrated; then + log "Natywna instancja już działa na porcie $NATIVE_CURRENT_PORT (nie 3306) - pomijam przejęcie." + exit 0 + fi + + check_native_reachable + preflight_new_port_free + detect_native_service_name + + # NOTE: deliberately an EXIT trap, not an ERR trap as originally planned. + # die() (used by every failure path below) calls `exit` directly, and bash's + # ERR trap does not fire for a function that terminates via an explicit + # `exit` call - it only fires when a command's non-zero status propagates + # through normal control flow. An EXIT trap fires on *any* shell exit + # (explicit exit, errexit-triggered exit, or falling off the end of the + # script), which is what "any failure from this point on triggers + # rollback" actually requires here. ROLLBACK_ARMED guards it so it is a + # no-op before this point and after a successful run. + ROLLBACK_ARMED=1 + trap 'rollback_service_state' EXIT + + disable_custombuild_mysql_management + apply_config_mutations + restart_native_service + verify_native_listening_new_port + restart_directadmin_service + verify_da_connectivity_new_port + + ROLLBACK_ARMED=0 + trap - EXIT + log "Przejęcie portu natywnej instancji MariaDB zakończone powodzeniem: $NATIVE_CURRENT_PORT -> $NEW_NATIVE_PORT." +} + [ -r "$COMMON_FILE" ] || die "missing helper: $COMMON_FILE" # shellcheck source=./mysql_common.sh source "$COMMON_FILE" if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then - : # main() is added in Task 4. + main "$@" fi diff --git a/tests/native_mariadb_port_takeover_e2e_test.sh b/tests/native_mariadb_port_takeover_e2e_test.sh new file mode 100644 index 0000000..d43a082 --- /dev/null +++ b/tests/native_mariadb_port_takeover_e2e_test.sh @@ -0,0 +1,203 @@ +#!/bin/bash +set -euo pipefail + +PLUGIN_DIR="$(cd "$(dirname "$0")/.." && pwd)" +SCRIPT="$PLUGIN_DIR/scripts/setup/native_mariadb_port_takeover.sh" +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TMP_DIR"' EXIT + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +setup_fixture() { + local dir="$1" + mkdir -p "$dir/bin" "$dir/my.cnf.d" + + cat > "$dir/my.cnf" <<'EOF' +[client] +port=3306 + +[mysqld] +datadir=/var/lib/mysql +port=3306 +socket=/var/lib/mysql/mysql.sock +EOF + + cat > "$dir/mysql.conf" <<'EOF' +user=da_admin +passwd=secret +port=3306 +socket=/var/lib/mysql/mysql.sock +host= +EOF + + cat > "$dir/options.conf" <<'EOF' +mysql_inst=mariadb +mariadb=10.6 +EOF + + cat > "$dir/os-release" <<'EOF' +ID="almalinux" +VERSION_ID="9.4" +EOF +} + +# run_takeover DIR +# Runs the script under test against the fixture already prepared at DIR +# (by a prior call to setup_fixture, possibly customized further by the +# scenario since) - writes stdout+stderr to "$DIR/run.out". Does NOT touch +# the fixture itself, so scenario-specific customizations made after +# setup_fixture (e.g. editing mysql.conf, swapping os-release) are preserved. +run_takeover() { + local dir="$1" + + env \ + NATIVE_TAKEOVER_LOG="$dir/takeover.log" \ + NATIVE_MY_CNF="$dir/my.cnf" \ + NATIVE_MY_CNF_D="$dir/my.cnf.d" \ + NATIVE_MYSQL_CONF="$dir/mysql.conf" \ + CUSTOMBUILD_OPTIONS_CONF="$dir/options.conf" \ + OS_RELEASE_FILE="$dir/os-release" \ + NEW_NATIVE_PORT="3307" \ + DA_CLI_BIN="$dir/bin/da" \ + PATH="$dir/bin:$PATH" \ + bash "$SCRIPT" > "$dir/run.out" 2>&1 +} + +# --- Fake command stubs shared by scenarios; each scenario builds its own copies --- +write_common_stubs() { + local dir="$1" + local mode="${2:-happy}" + + cat > "$dir/bin/da" <> "$dir/da_calls.log" +exit 0 +EOF + chmod 755 "$dir/bin/da" + + cat > "$dir/bin/mysql" </dev/null || echo 3306)" ]; then + exit 0 +fi +exit 1 +EOF + chmod 755 "$dir/bin/mysql" + echo "3306" > "$dir/current_port" + + cat > "$dir/bin/systemctl" <.service' and picks the first +# that succeeds - only mariadb.service "exists" here, matching a real +# AlmaLinux+MariaDB box where mysqld.service does not exist. +if [ "\$1" = "list-unit-files" ]; then + case "\$2" in + mariadb.service) exit 0 ;; + *) exit 1 ;; + esac +fi + +echo "systemctl \$*" >> "$dir/systemctl_calls.log" +if [ "\$1" = "restart" ] && [ "\$2" = "mariadb" ] && [ "$mode" = "restart-fail" ]; then + exit 1 +fi +if [ "\$1" = "restart" ] && [ "\$2" = "mariadb" ]; then + echo "3307" > "$dir/current_port" +fi +if [ "\$1" = "restart" ] && [ "\$2" = "directadmin" ] && [ "$mode" = "da-restart-fail" ]; then + exit 1 +fi +exit 0 +EOF + chmod 755 "$dir/bin/systemctl" + + cat > "$dir/bin/ss" </dev/null || echo 3306)" +for arg in "\$@"; do + case "\$arg" in + *":\$current"*) echo header; echo occupied; exit 0 ;; + esac +done +exit 0 +EOF + chmod 755 "$dir/bin/ss" +} + +# --- Scenario 1: happy path --- +DIR="$TMP_DIR/happy" +setup_fixture "$DIR" +write_common_stubs "$DIR" happy +run_takeover "$DIR" +STATUS=$? +[ "$STATUS" -eq 0 ] || fail "happy path should exit 0, got $STATUS: $(cat "$DIR/run.out")" +grep -Fxq "port=3307" "$DIR/mysql.conf" || fail "happy path should leave mysql.conf on port 3307" +grep -Fq "build set mysql_inst no" "$DIR/da_calls.log" || fail "happy path should call da build set mysql_inst no" +grep -Fq "restart mariadb" "$DIR/systemctl_calls.log" || fail "happy path should restart mariadb" +grep -Fq "restart directadmin" "$DIR/systemctl_calls.log" || fail "happy path should restart directadmin" + +# --- Scenario 2: already migrated (idempotent no-op) --- +DIR="$TMP_DIR/already" +setup_fixture "$DIR" +write_common_stubs "$DIR" happy +sed -i.orig 's/^port=3306$/port=3307/' "$DIR/mysql.conf" +run_takeover "$DIR" +STATUS=$? +[ "$STATUS" -eq 0 ] || fail "already-migrated case should exit 0, got $STATUS" +grep -Fq "build set" "$DIR/da_calls.log" 2>/dev/null && fail "already-migrated case should not touch CustomBuild settings" + +# --- Scenario 3: OS guard rejection --- +DIR="$TMP_DIR/badose" +setup_fixture "$DIR" +write_common_stubs "$DIR" happy +cat > "$DIR/os-release" <<'EOF' +ID="cloudlinux" +VERSION_ID="8.9" +EOF +set +e +run_takeover "$DIR" +STATUS=$? +set -e +[ "$STATUS" -ne 0 ] || fail "CloudLinux should be refused, not exit 0" +grep -Fq "build set" "$DIR/da_calls.log" 2>/dev/null && fail "OS-guard rejection must not touch CustomBuild settings" + +# --- Scenario 4: rollback when native restart fails --- +DIR="$TMP_DIR/restartfail" +setup_fixture "$DIR" +write_common_stubs "$DIR" restart-fail +set +e +run_takeover "$DIR" +STATUS=$? +set -e +[ "$STATUS" -ne 0 ] || fail "a failed native restart should not exit 0" +grep -Fxq "port=3306" "$DIR/mysql.conf" \ + || fail "mysql.conf should be rolled back to port 3306 after a failed native restart" +grep -Fq "build set mysql_inst mariadb" "$DIR/da_calls.log" \ + || fail "mysql_inst should be restored to mariadb after a failed native restart" + +# --- Scenario 5: rollback when DirectAdmin restart fails --- +DIR="$TMP_DIR/darestartfail" +setup_fixture "$DIR" +write_common_stubs "$DIR" da-restart-fail +set +e +run_takeover "$DIR" +STATUS=$? +set -e +[ "$STATUS" -ne 0 ] || fail "a failed directadmin restart should not exit 0" +grep -Fxq "port=3306" "$DIR/mysql.conf" \ + || fail "mysql.conf should be rolled back to port 3306 after a failed directadmin restart" +grep -Fq "build set mysql_inst mariadb" "$DIR/da_calls.log" \ + || fail "mysql_inst should be restored to mariadb after a failed directadmin restart" + +echo "native_mariadb_port_takeover_e2e_test: OK"