diff --git a/config-php-fpm.sh b/config-php-fpm.sh new file mode 100755 index 0000000..61ef0e0 --- /dev/null +++ b/config-php-fpm.sh @@ -0,0 +1,438 @@ +#!/usr/bin/env bash +# ============================================================================== +# tune-php-fpm.sh +# Setzt optimierte Werte in php.ini und fpm.conf fuer mehrere PHP-Versionen +# und startet den jeweiligen PHP-FPM-Service neu. +# +# Betroffene PHP-Hauptversionen: 8.3, 8.4, 8.5 +# Symlink-Basis: /usr/local/php- -> /usr/local/php- +# ============================================================================== + +set -euo pipefail + +# ------------------------------------------------------------------------------ +# Konfiguration +# ------------------------------------------------------------------------------ +PHP_MAIN_VERSIONS=("8.2" "8.3" "8.4" "8.5") + +APC_SHM_SIZE="128M" +PM_MAX_REQUESTS=5000 +PM_MAX_CHILDREN=60 + +# Wird durch interaktive Abfrage befuellt: +CONFIGURE_LOGLEVEL=false +FPM_LOG_LEVEL="" + +# ------------------------------------------------------------------------------ +# Farben & Ausgabe +# ------------------------------------------------------------------------------ +C_RESET="\e[0m" +C_BOLD="\e[1m" +C_BLUE="\e[34m" +C_GREEN="\e[32m" +C_YELLOW="\e[33m" +C_RED="\e[31m" +C_CYAN="\e[36m" +C_GRAY="\e[90m" + +info() { echo -e "${C_BLUE}[INFO]${C_RESET} $*"; } +ok() { echo -e "${C_GREEN}[OK]${C_RESET} $*"; } +skip() { echo -e "${C_GRAY}[SKIP]${C_RESET} $*"; } +warn() { echo -e "${C_YELLOW}[WARN]${C_RESET} $*"; } +error() { echo -e "${C_RED}[ERROR]${C_RESET} $*" >&2; } +changed() { echo -e "${C_GREEN}[SET]${C_RESET} $*"; } + +hr() { + echo -e "${C_GRAY}--------------------------------------------------------------${C_RESET}" +} + +section() { + echo "" + echo -e "${C_BOLD}${C_CYAN}+- PHP $1 -----------------------------------------------------${C_RESET}" +} + +# ------------------------------------------------------------------------------ +# INI-Hilfsfunktionen +# ------------------------------------------------------------------------------ + +# Liest den aktuell aktiven (nicht auskommentierten) Wert eines Keys aus einer INI-Datei. +# Gibt leeren String zurueck wenn nicht aktiv gesetzt. +get_ini_value() { + local file="$1" + local key="$2" + # Nur aktive Zeilen (nicht auskommentiert), letzten Treffer nehmen + local val + val=$(grep -E "^\s*${key}\s*=" "$file" 2>/dev/null | tail -1 \ + | sed -E "s/^\s*${key}\s*=\s*//" | sed 's/[[:space:]]*$//' || true) + echo "$val" +} + +# Setzt einen Wert in einer INI-Datei. +# Ueberspringt wenn der Wert bereits korrekt gesetzt ist. +# Ersetzt aktive oder auskommentierte Zeilen; fuegt ans Ende an falls nicht vorhanden. +# +# Gibt zurueck: 0 = geaendert, 1 = uebersprungen, 2 = Fehler +set_ini_value() { + local file="$1" + local key="$2" + local desired="$3" + + if [[ ! -f "$file" ]]; then + error "Datei nicht gefunden: $file" + return 2 + fi + + # Aktuellen Wert lesen + local current + current=$(get_ini_value "$file" "$key") + + # Bereits korrekt gesetzt? + if [[ "$current" == "$desired" ]]; then + skip "${key} = ${desired} (bereits korrekt, keine Aenderung)" + return 1 + fi + + # Backup anlegen (einmal pro Datei und Skriptlauf) + local backup="${file}.bak-${BACKUP_TS}" + if [[ ! -f "$backup" ]]; then + cp "$file" "$backup" + fi + + if grep -qE "^\s*;?\s*${key}\s*=" "$file"; then + # Zeile vorhanden (aktiv oder auskommentiert) -> ersetzen + sed -i -E "0,/^\s*;?\s*${key}\s*=/{s|^\s*;?\s*${key}\s*=.*|${key} = ${desired}|}" "$file" + if [[ -n "$current" ]]; then + changed "${key} = ${desired} (war: ${current})" + else + changed "${key} = ${desired} (war auskommentiert)" + fi + else + # Key fehlt komplett -> anhaengen + echo "" >> "$file" + echo "${key} = ${desired}" >> "$file" + changed "${key} = ${desired} (neu eingefuegt)" + fi + return 0 +} + +# Setzt einen Wert gezielt im [global]-Abschnitt einer php-fpm.conf. +# log_level und andere globale FPM-Direktiven sind NUR dort gueltig -- +# am Dateiende anhaengen wuerde den Wert in den Pool-Kontext setzen -> Fehler. +# +# Strategie: +# 1. Key bereits aktiv in [global] -> ersetzen (wie set_ini_value) +# 2. Key auskommentiert in [global] -> aktivieren und Wert setzen +# 3. Key fehlt -> nach der "[global]"-Zeile einfuegen +# 4. Kein [global]-Abschnitt -> Fehler +# +# Gibt zurueck: 0 = geaendert, 1 = uebersprungen, 2 = Fehler +set_global_fpm_value() { + local file="$1" + local key="$2" + local desired="$3" + + if [[ ! -f "$file" ]]; then + error "Datei nicht gefunden: $file" + return 2 + fi + + if ! grep -qE "^\s*\[global\]" "$file"; then + error "Kein [global]-Abschnitt in ${file} gefunden -- Abbruch." + return 2 + fi + + # Aktuellen aktiven Wert lesen + local current + current=$(get_ini_value "$file" "$key") + + if [[ "$current" == "$desired" ]]; then + skip "${key} = ${desired} (bereits korrekt, keine Aenderung)" + return 1 + fi + + # Backup anlegen + local backup="${file}.bak-${BACKUP_TS}" + if [[ ! -f "$backup" ]]; then + cp "$file" "$backup" + fi + + # Pruefen ob Key irgendwo in der Datei vorkommt (aktiv oder auskommentiert) + if grep -qE "^\s*;?\s*${key}\s*=" "$file"; then + # Zeile vorhanden -> einfach ersetzen (erste Fundstelle, egal in welchem Abschnitt) + sed -i -E "0,/^\s*;?\s*${key}\s*=/{s|^\s*;?\s*${key}\s*=.*|${key} = ${desired}|}" "$file" + if [[ -n "$current" ]]; then + changed "${key} = ${desired} (war: ${current})" + else + changed "${key} = ${desired} (war auskommentiert)" + fi + else + # Key fehlt komplett -> nach der [global]-Zeile einfuegen, + # mit Kommentar davor und Leerzeile danach. + sed -i "/^\s*\[global\]/a \\\n; Minimaler Log-Level fuer FPM-eigene Meldungen (Standard: notice).\n; Gueltige Werte: alert, error, warning, notice, debug\n${key} = ${desired}\n" "$file" + changed "${key} = ${desired} (in [global] eingefuegt)" + fi + return 0 +} + +# Entfernt eine faelschlicherweise ausserhalb von [global] eingefuegte Direktive. +# Wird als Reparaturschritt verwendet wenn das Skript den Wert zuvor ans Ende +# der Datei angehaengt hat. +repair_misplaced_key() { + local file="$1" + local key="$2" + + if [[ ! -f "$file" ]]; then + return + fi + + # Zeile zaehlen die den Key enthalten + local count + count=$(grep -cE "^\s*${key}\s*=" "$file" || true) + + if [[ "$count" -gt 1 ]]; then + warn "Reparatur: ${count}x '${key}' in ${file} gefunden -- entferne Duplikate." + # Backup falls noch nicht vorhanden + local backup="${file}.bak-${BACKUP_TS}" + if [[ ! -f "$backup" ]]; then + cp "$file" "$backup" + fi + # Alle Vorkommen loeschen ausser dem ersten + awk -v key="^[[:space:]]*${key}[[:space:]]*=" ' + $0 ~ key { if (!seen++) print; next } + { print } + ' "$file" > "${file}.tmp" && mv "${file}.tmp" "$file" + ok "Duplikate entfernt." + fi +} + +# ------------------------------------------------------------------------------ +# Interaktive Log-Level-Abfrage +# ------------------------------------------------------------------------------ +ask_loglevel() { + echo "" + echo -e "${C_BOLD}+--------------------------------------------------------------+${C_RESET}" + echo -e "${C_BOLD}| PHP-FPM Log-Level konfigurieren |${C_RESET}" + echo -e "${C_BOLD}+--------------------------------------------------------------+${C_RESET}" + echo "" + echo -e " Der Standard-Log-Level ${C_BOLD}notice${C_RESET} erzeugt die bekannten" + echo -e " Massen-Eintraege (child started / exited) im FPM-Error-Log." + echo "" + echo -e " Soll der Log-Level in ${C_BOLD}php-fpm.conf${C_RESET} (globale Sektion)" + echo -e " fuer alle Versionen konfiguriert werden?" + echo "" + echo -e " ${C_BOLD}[j]${C_RESET} Ja, Log-Level setzen" + echo -e " ${C_BOLD}[n]${C_RESET} Nein, Log-Level unveraendert lassen" + echo "" + while true; do + read -r -p " Auswahl [j/n]: " answer + case "${answer,,}" in + j|ja|y|yes) + CONFIGURE_LOGLEVEL=true + break + ;; + n|nein|no) + CONFIGURE_LOGLEVEL=false + info "Log-Level wird nicht veraendert." + return + ;; + *) + warn "Bitte 'j' oder 'n' eingeben." + ;; + esac + done + + echo "" + echo -e " ${C_BOLD}Verfuegbare Log-Level${C_RESET} (von ruhig -> ausfuehrlich):" + echo "" + echo -e " ${C_BOLD}[1]${C_RESET} ${C_GREEN}warning${C_RESET} Nur Warnungen und schlimmer" + echo -e " ${C_GRAY}-> Empfohlen: unterdrueckt child-started/exited Meldungen${C_RESET}" + echo -e " ${C_BOLD}[2]${C_RESET} ${C_YELLOW}error${C_RESET} Nur Fehler (alert + error)" + echo -e " ${C_BOLD}[3]${C_RESET} ${C_RED}alert${C_RESET} Nur kritische Meldungen" + echo -e " ${C_BOLD}[4]${C_RESET} notice Standard - inkl. child started/exited" + echo -e " ${C_BOLD}[5]${C_RESET} debug Sehr ausfuehrlich (nur zur Fehlersuche)" + echo "" + + while true; do + read -r -p " Level waehlen [1-5, Standard: 1]: " choice + choice="${choice:-1}" + case "$choice" in + 1) FPM_LOG_LEVEL="warning"; break ;; + 2) FPM_LOG_LEVEL="error"; break ;; + 3) FPM_LOG_LEVEL="alert"; break ;; + 4) FPM_LOG_LEVEL="notice"; break ;; + 5) FPM_LOG_LEVEL="debug"; break ;; + *) warn "Bitte eine Zahl zwischen 1 und 5 eingeben." ;; + esac + done + + echo "" + ok "Log-Level wird gesetzt auf: ${C_BOLD}${FPM_LOG_LEVEL}${C_RESET}" +} + +# ------------------------------------------------------------------------------ +# Root-Check +# ------------------------------------------------------------------------------ +if [[ $EUID -ne 0 ]]; then + error "Dieses Skript muss als root ausgefuehrt werden." + exit 1 +fi + +# Einheitlicher Backup-Zeitstempel fuer diesen Skriptlauf +BACKUP_TS="$(date +%Y%m%d%H%M%S)" + +# ------------------------------------------------------------------------------ +# Interaktive Abfragen (vor der eigentlichen Verarbeitung) +# ------------------------------------------------------------------------------ +ask_loglevel + +echo "" +hr +echo -e " ${C_BOLD}Zusammenfassung der geplanten Aenderungen:${C_RESET}" +hr +echo -e " php.ini apc.shm_size = ${C_BOLD}${APC_SHM_SIZE}${C_RESET}" +echo -e " fpm pool pm.max_requests = ${C_BOLD}${PM_MAX_REQUESTS}${C_RESET}" +echo -e " fpm pool pm.max_children = ${C_BOLD}${PM_MAX_CHILDREN}${C_RESET}" +if [[ "$CONFIGURE_LOGLEVEL" == true ]]; then + echo -e " php-fpm.conf log_level = ${C_BOLD}${FPM_LOG_LEVEL}${C_RESET}" +fi +echo -e " Versionen: ${C_BOLD}${PHP_MAIN_VERSIONS[*]}${C_RESET}" +hr +echo "" +read -r -p " Fortfahren? [j/n]: " confirm +case "${confirm,,}" in + j|ja|y|yes) echo "" ;; + *) + info "Abgebrochen, keine Aenderungen vorgenommen." + exit 0 + ;; +esac + +# ------------------------------------------------------------------------------ +# Hauptverarbeitung +# ------------------------------------------------------------------------------ +ERRORS=0 +CHANGED=0 +SKIPPED=0 +NEEDS_RESTART=false + +for MAIN_VER in "${PHP_MAIN_VERSIONS[@]}"; do + + section "$MAIN_VER" + + SYMLINK_DIR="/usr/local/php-${MAIN_VER}" + + if [[ ! -d "$SYMLINK_DIR" ]]; then + warn "Verzeichnis/Symlink nicht gefunden: ${SYMLINK_DIR} - ueberspringe Version." + continue + fi + + REAL_DIR="$(realpath "$SYMLINK_DIR")" + info "Echter Pfad: ${REAL_DIR}" + + PHP_INI="${SYMLINK_DIR}/etc/php.ini" + FPM_CONF="${SYMLINK_DIR}/etc/fpm.d/www-${MAIN_VER}.php-fpm.conf" + FPM_GLOBAL_CONF="${SYMLINK_DIR}/etc/php-fpm.conf" + SERVICE="php-${MAIN_VER}-fpm" + SERVICE_FILE="/etc/systemd/system/${SERVICE}.service" + VERSION_CHANGED=false + + # --- php.ini --- + echo "" + info "php.ini: ${PHP_INI}" + if [[ ! -f "$PHP_INI" ]]; then + error "Datei nicht gefunden: ${PHP_INI}" + ((ERRORS++)) || true + else + if set_ini_value "$PHP_INI" "apc.shm_size" "$APC_SHM_SIZE"; then + VERSION_CHANGED=true; ((CHANGED++)) || true + fi + fi + + # --- Pool fpm.conf --- + echo "" + info "Pool-Conf: ${FPM_CONF}" + if [[ ! -f "$FPM_CONF" ]]; then + error "Datei nicht gefunden: ${FPM_CONF}" + ((ERRORS++)) || true + else + local_changed=false + if set_ini_value "$FPM_CONF" "pm.max_requests" "$PM_MAX_REQUESTS"; then + local_changed=true + fi + if set_ini_value "$FPM_CONF" "pm.max_children" "$PM_MAX_CHILDREN"; then + local_changed=true + fi + if $local_changed; then + VERSION_CHANGED=true; ((CHANGED++)) || true + fi + fi + + # --- Log-Level in globaler php-fpm.conf --- + if [[ "$CONFIGURE_LOGLEVEL" == true ]]; then + echo "" + info "FPM-Global: ${FPM_GLOBAL_CONF}" + if [[ ! -f "$FPM_GLOBAL_CONF" ]]; then + warn "Globale php-fpm.conf nicht gefunden: ${FPM_GLOBAL_CONF}" + warn "Log-Level konnte fuer PHP ${MAIN_VER} nicht gesetzt werden." + ((ERRORS++)) || true + else + # Reparatur: frueherer Skript-Lauf hat log_level evtl. ans Ende + # der Datei (ausserhalb [global]) angehaengt -> Duplikate entfernen + repair_misplaced_key "$FPM_GLOBAL_CONF" "log_level" + # log_level MUSS im [global]-Abschnitt stehen + if set_global_fpm_value "$FPM_GLOBAL_CONF" "log_level" "$FPM_LOG_LEVEL"; then + VERSION_CHANGED=true; ((CHANGED++)) || true + fi + fi + fi + + # --- Service-Aktion --- + echo "" + if [[ ! -f "$SERVICE_FILE" ]]; then + warn "Service-Datei nicht gefunden: ${SERVICE_FILE} - kein Neustart." + ((ERRORS++)) || true + continue + fi + + if ! $VERSION_CHANGED; then + skip "Keine Aenderungen fuer PHP ${MAIN_VER} - Service-Neustart nicht noetig." + ((SKIPPED++)) || true + continue + fi + + # apc.shm_size erfordert immer restart (Shared Memory) + info "Starte Service neu: ${SERVICE}" + if systemctl restart "${SERVICE}"; then + ok "Service ${SERVICE} neu gestartet." + NEEDS_RESTART=false + else + error "Neustart fehlgeschlagen: ${SERVICE}" + ((ERRORS++)) || true + fi + +done + +# ------------------------------------------------------------------------------ +# Abschluss +# ------------------------------------------------------------------------------ +echo "" +echo -e "${C_BOLD}+--------------------------------------------------------------+${C_RESET}" +echo -e "${C_BOLD}| Abschlussbericht |${C_RESET}" +echo -e "${C_BOLD}+--------------------------------------------------------------+${C_RESET}" +echo "" +echo -e " Geaenderte Werte: ${C_BOLD}${CHANGED}${C_RESET}" +echo -e " Uebersprungen: ${C_BOLD}${SKIPPED}${C_RESET} (bereits korrekt gesetzt)" +if [[ $ERRORS -gt 0 ]]; then + echo -e " Fehler/Warnungen: ${C_RED}${C_BOLD}${ERRORS}${C_RESET}" +else + echo -e " Fehler/Warnungen: ${C_GREEN}${C_BOLD}0${C_RESET}" +fi +echo "" +if [[ $ERRORS -eq 0 ]]; then + ok "Fertig - alle Versionen erfolgreich verarbeitet." +else + warn "Fertig mit ${ERRORS} Warnung(en)/Fehler(n) - bitte Ausgabe pruefen." +fi +echo "" +echo -e " ${C_GRAY}Backups mit Suffix .bak-${BACKUP_TS} angelegt.${C_RESET}" +echo ""