feat: add service restart, verification, and full rollback orchestration to native port takeover

This commit is contained in:
Marek Miklewicz
2026-07-05 21:47:51 +02:00
parent 35b9c1af78
commit 42ec50afe4
2 changed files with 304 additions and 1 deletions
+101 -1
View File
@@ -234,10 +234,110 @@ rollback_config_changes() {
done 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" [ -r "$COMMON_FILE" ] || die "missing helper: $COMMON_FILE"
# shellcheck source=./mysql_common.sh # shellcheck source=./mysql_common.sh
source "$COMMON_FILE" source "$COMMON_FILE"
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
: # main() is added in Task 4. main "$@"
fi fi
@@ -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" <<EOF
#!/bin/bash
echo "\$*" >> "$dir/da_calls.log"
exit 0
EOF
chmod 755 "$dir/bin/da"
cat > "$dir/bin/mysql" <<EOF
#!/bin/bash
CURRENT_PORT_FILE="$dir/current_port"
requested_port=""
for arg in "\$@"; do
case "\$arg" in
--port=*) requested_port="\${arg#--port=}" ;;
esac
done
if [ "\$requested_port" = "\$(cat "\$CURRENT_PORT_FILE" 2>/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" <<EOF
#!/bin/bash
# mysql_service_name() (mysql_common.sh) probes "mysqld", then "mariadb", then
# "mysql" via 'systemctl list-unit-files <name>.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" <<EOF
#!/bin/bash
current="\$(cat "$dir/current_port" 2>/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"