#!/bin/bash set -Eeuo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PLUGIN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" COMMON_FILE="$SCRIPT_DIR/mysql_common.sh" NATIVE_MY_CNF="${NATIVE_MY_CNF:-/etc/my.cnf}" NATIVE_MY_CNF_D="${NATIVE_MY_CNF_D:-/etc/my.cnf.d}" NATIVE_MYSQL_CONF="${NATIVE_MYSQL_CONF:-/usr/local/directadmin/conf/mysql.conf}" CUSTOMBUILD_OPTIONS_CONF="${CUSTOMBUILD_OPTIONS_CONF:-/usr/local/directadmin/custombuild/options.conf}" NEW_NATIVE_PORT="${NEW_NATIVE_PORT:-3307}" DA_CLI_BIN="${DA_CLI_BIN:-da}" OS_RELEASE_FILE="${OS_RELEASE_FILE:-/etc/os-release}" LOG_FILE="${NATIVE_TAKEOVER_LOG:-$PLUGIN_DIR/data/logs/native-port-takeover.log}" mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null || true log() { printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >> "$LOG_FILE" printf '[INFO] %s\n' "$*" } die() { log "ERROR: $*" printf '[BŁĄD] %s\n' "$*" >&2 exit 1 } # --- state populated by the detection functions, consumed by later steps --- NATIVE_CURRENT_PORT="" NATIVE_CURRENT_SOCKET="" NATIVE_CURRENT_HOST="" NATIVE_CURRENT_USER="" NATIVE_CURRENT_PASS="" NATIVE_CURRENT_MYSQL_INST="" require_root() { [ "$(id -u)" -eq 0 ] || die "Ten skrypt musi zostać uruchomiony jako root." } check_os_guard() { [ -r "$OS_RELEASE_FILE" ] || die "Nie można odczytać $OS_RELEASE_FILE." local os_id="" os_version="" os_id="$(awk -F= '$1=="ID"{gsub(/"/,"",$2); print $2}' "$OS_RELEASE_FILE")" os_version="$(awk -F= '$1=="VERSION_ID"{gsub(/"/,"",$2); print $2}' "$OS_RELEASE_FILE")" [ "$os_id" = "almalinux" ] || die "Ten skrypt obsługuje wyłącznie AlmaLinux 8/9 (wykryto: ${os_id:-nieznany})." case "$os_version" in 8*|9*) ;; *) die "Ten skrypt obsługuje wyłącznie AlmaLinux 8/9 (wykryto wersję: ${os_version:-nieznana})." ;; esac log "Wykryto obsługiwany system: AlmaLinux $os_version." } read_native_mysql_conf() { [ -r "$NATIVE_MYSQL_CONF" ] || die "Nie znaleziono pliku $NATIVE_MYSQL_CONF." NATIVE_CURRENT_USER="$(mysql_read_key_value "$NATIVE_MYSQL_CONF" user || true)" NATIVE_CURRENT_PASS="$(mysql_read_key_value "$NATIVE_MYSQL_CONF" passwd || true)" NATIVE_CURRENT_PORT="$(mysql_read_key_value "$NATIVE_MYSQL_CONF" port || true)" NATIVE_CURRENT_SOCKET="$(mysql_read_key_value "$NATIVE_MYSQL_CONF" socket || true)" NATIVE_CURRENT_HOST="$(mysql_read_key_value "$NATIVE_MYSQL_CONF" host || true)" [ -n "$NATIVE_CURRENT_USER" ] || die "Nie udało się odczytać pola user z $NATIVE_MYSQL_CONF." [ -n "$NATIVE_CURRENT_PORT" ] || die "Nie udało się odczytać pola port z $NATIVE_MYSQL_CONF." } read_current_mysql_inst() { [ -r "$CUSTOMBUILD_OPTIONS_CONF" ] || die "Nie znaleziono pliku $CUSTOMBUILD_OPTIONS_CONF." NATIVE_CURRENT_MYSQL_INST="$(mysql_read_key_value "$CUSTOMBUILD_OPTIONS_CONF" mysql_inst || true)" [ -n "$NATIVE_CURRENT_MYSQL_INST" ] || die "Nie udało się odczytać mysql_inst z $CUSTOMBUILD_OPTIONS_CONF." log "Bieżąca wartość mysql_inst: $NATIVE_CURRENT_MYSQL_INST." } already_migrated() { [ "$NATIVE_CURRENT_PORT" != "3306" ] } check_native_reachable() { local bin bin="$(mysql_detect_binary mysql)" || die "Nie znaleziono klienta mysql/mariadb do sprawdzenia natywnej instancji." MYSQL_PWD="$NATIVE_CURRENT_PASS" "$bin" \ --user="$NATIVE_CURRENT_USER" \ --protocol=TCP --host=127.0.0.1 --port="$NATIVE_CURRENT_PORT" \ -e "SELECT 1" >/dev/null 2>>"$LOG_FILE" \ || die "Natywna instancja MariaDB nie odpowiada na porcie $NATIVE_CURRENT_PORT z bieżącymi poświadczeniami." log "Natywna instancja MariaDB odpowiada na porcie $NATIVE_CURRENT_PORT." } port_is_listening() { local port="$1" if command -v ss >/dev/null 2>&1; then ss -ltn "( sport = :$port )" 2>/dev/null | awk 'NR > 1 { found=1 } END { exit !found }' return $? fi if command -v lsof >/dev/null 2>&1; then lsof -nP -iTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1 return $? fi log "Nie znaleziono ss ani lsof - zakładam, że port $port jest wolny." return 1 } preflight_new_port_free() { if port_is_listening "$NEW_NATIVE_PORT"; then die "Port $NEW_NATIVE_PORT jest już zajęty. Zmień NEW_NATIVE_PORT i spróbuj ponownie." fi log "Port docelowy $NEW_NATIVE_PORT jest wolny." } DID_SET_MYSQL_INST_NO=0 CONFIG_BACKUPS=() disable_custombuild_mysql_management() { if [ "$NATIVE_CURRENT_MYSQL_INST" = "no" ]; then log "mysql_inst jest już ustawione na 'no' - pomijam." return 0 fi "$DA_CLI_BIN" build set mysql_inst no >>"$LOG_FILE" 2>&1 \ || die "Nie udało się ustawić mysql_inst=no przez '$DA_CLI_BIN build set mysql_inst no'." DID_SET_MYSQL_INST_NO=1 log "Ustawiono mysql_inst=no przez CustomBuild." } restore_custombuild_mysql_management() { [ "$DID_SET_MYSQL_INST_NO" -eq 1 ] || return 0 "$DA_CLI_BIN" build set mysql_inst "$NATIVE_CURRENT_MYSQL_INST" >>"$LOG_FILE" 2>&1 \ || log "OSTRZEŻENIE: nie udało się przywrócić mysql_inst=$NATIVE_CURRENT_MYSQL_INST." log "Przywrócono mysql_inst=$NATIVE_CURRENT_MYSQL_INST." } backup_config_file() { local file="$1" local backup backup="${file}.bak.$(date +%Y%m%d%H%M%S)" cp -a "$file" "$backup" CONFIG_BACKUPS+=("$file:$backup") log "Utworzono kopię zapasową $file jako $backup." } rewrite_port_in_file() { local file="$1" new_port="$2" local tmp changed=0 [ -r "$file" ] || return 1 tmp="$(mktemp)" awk -v new_port="$new_port" ' BEGIN { in_section = 0; changed = 0 } /^[[:space:]]*\[[^]]+\][[:space:]]*$/ { section = tolower($0) gsub(/^[[:space:]]*\[[[:space:]]*/, "", section) gsub(/[[:space:]]*\][[:space:]]*$/, "", section) in_section = (section == "mysqld" || section == "mariadb" || section == "server") print next } in_section && /^[[:space:]]*port[[:space:]]*=/ { print "port=" new_port changed = 1 next } { print } END { if (changed) exit 0; else exit 1 } ' "$file" > "$tmp" local awk_status=$? if [ "$awk_status" -eq 0 ]; then cp "$tmp" "$file" changed=1 fi rm -f "$tmp" [ "$changed" -eq 1 ] } find_and_rewrite_native_port_configs() { local any_changed=0 local f for f in "$NATIVE_MY_CNF" "$NATIVE_MY_CNF_D"/*.cnf; do [ -f "$f" ] || continue if grep -Eq '^[[:space:]]*port[[:space:]]*=[[:space:]]*3306[[:space:]]*$' "$f"; then backup_config_file "$f" if rewrite_port_in_file "$f" "$NEW_NATIVE_PORT"; then any_changed=1 log "Zaktualizowano port w $f." fi fi done if [ "$any_changed" -eq 0 ]; then backup_config_file "$NATIVE_MY_CNF" if grep -Eq '^[[:space:]]*\[mysqld\][[:space:]]*$' "$NATIVE_MY_CNF"; then awk -v new_port="$NEW_NATIVE_PORT" ' { print } /^[[:space:]]*\[mysqld\][[:space:]]*$/ && !done { print "port=" new_port; done=1 } ' "$NATIVE_MY_CNF" > "${NATIVE_MY_CNF}.tmp" && mv "${NATIVE_MY_CNF}.tmp" "$NATIVE_MY_CNF" else printf '\n[mysqld]\nport=%s\n' "$NEW_NATIVE_PORT" >> "$NATIVE_MY_CNF" fi log "Brak istniejącej dyrektywy port= w sekcji serwera - dodano port=$NEW_NATIVE_PORT do $NATIVE_MY_CNF." fi } update_native_mysql_conf_port() { local target_port="$1" local tmp tmp="$(mktemp)" awk -v new_port="$target_port" ' BEGIN { done = 0 } /^[[:space:]]*port[[:space:]]*=/ { print "port=" new_port; done=1; next } { print } END { if (!done) print "port=" new_port } ' "$NATIVE_MYSQL_CONF" > "$tmp" cp "$tmp" "$NATIVE_MYSQL_CONF" rm -f "$tmp" } apply_config_mutations() { backup_config_file "$NATIVE_MYSQL_CONF" find_and_rewrite_native_port_configs update_native_mysql_conf_port "$NEW_NATIVE_PORT" log "Zaktualizowano $NATIVE_MYSQL_CONF na port $NEW_NATIVE_PORT." } rollback_config_changes() { local pair file backup for pair in "${CONFIG_BACKUPS[@]}"; do file="${pair%%:*}" backup="${pair#*:}" if [ -r "$backup" ]; then cp -a "$backup" "$file" log "Przywrócono $file z kopii zapasowej $backup." fi 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 "$@" fi