#!/bin/bash # # install_redis.sh # Install and configure Redis server for WordPress on LLSMP/AlmaLinux. # # Usage: # bash install_redis.sh -- full install+configure flow # bash install_redis.sh --status -- status check only (no install) # # Flow: # 1. Check if Redis is already installed/running -> report and exit early if healthy # 2. Install redis package via dnf (idempotent) # 3. Locate and patch redis.conf for Unix socket # 4. Grant litespeed user group access to the socket # 5. Enable + start the service, wait for socket readiness # 6. Verify PING via socket # 7. Configure WordPress LiteSpeed Cache Plugin Object Cache # set -uo pipefail # NOTE: Not using set -e globally so we can handle each error explicitly # and produce meaningful messages instead of silent exits. # ── Colors & helpers ───────────────────────────────────────────────────────── RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' info() { printf "${BLUE}[INFO] %s${NC}\n" "$*"; } success() { printf "${GREEN}[SUCCESS] %s${NC}\n" "$*"; } warning() { printf "${YELLOW}[WARNING] %s${NC}\n" "$*"; } error_exit(){ printf "${RED}[ERROR] %s${NC}\n" "$*" >&2; exit 1; } # ── Constants ──────────────────────────────────────────────────────────────── REDIS_SOCKET="/var/run/redis/redis.sock" REDIS_SOCKET_DIR="/var/run/redis" REDIS_CONF_PATHS=("/etc/redis/redis.conf" "/etc/redis.conf" "/etc/redis6/redis.conf" "/etc/redis7/redis.conf") WP_ROOT="/var/www/webroot/ROOT" WEB_USER="litespeed" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" STATUS_ONLY=false # ── Parse arguments ────────────────────────────────────────────────────────── for arg in "$@"; do case "$arg" in --status) STATUS_ONLY=true ;; esac done # ── Helper: detect redis service name ──────────────────────────────────────── get_redis_svc() { systemctl list-unit-files --type=service 2>/dev/null \ | grep -oP '[\w@-]*redis[\w@-]*\.service' \ | grep -v 'sentinel' \ | head -1 \ | sed 's/\.service//' } # ── Helper: detect redis.conf path ─────────────────────────────────────────── get_redis_conf() { for path in "${REDIS_CONF_PATHS[@]}"; do if [[ -f "$path" ]]; then echo "$path" return 0 fi done # Last resort: ask rpm local rpm_conf rpm_conf=$(rpm -ql redis 2>/dev/null | grep '\.conf$' | head -1) if [[ -n "$rpm_conf" && -f "$rpm_conf" ]]; then echo "$rpm_conf" return 0 fi echo "" } # ── Helper: test redis socket connectivity ─────────────────────────────────── test_redis_socket() { if [[ ! -S "$REDIS_SOCKET" ]]; then return 1 fi local result result=$(redis-cli -s "$REDIS_SOCKET" ping 2>/dev/null || true) [[ "$result" == "PONG" ]] } # ── STATUS CHECK ───────────────────────────────────────────────────────────── do_status() { local svc svc=$(get_redis_svc) if [[ -z "$svc" ]]; then echo "Redis: NOT INSTALLED" return 1 # signal caller that install is needed fi if systemctl is-active --quiet "$svc"; then if test_redis_socket; then success "Redis: RUNNING (service=$svc, socket=$REDIS_SOCKET, ping=PONG)" else warning "Redis: RUNNING (service=$svc) but socket unreachable at $REDIS_SOCKET" fi return 0 else local state state=$(systemctl is-enabled "$svc" 2>/dev/null || echo "unknown") warning "Redis: INSTALLED but STOPPED (service=$svc, enabled=$state)" return 2 # signal caller: installed but stopped fi } # ── STEP 1: Install redis package ──────────────────────────────────────────── step_install() { if command -v redis-server >/dev/null 2>&1; then info "Redis binary already present: $(command -v redis-server)" return 0 fi info "Redis not found. Installing via dnf..." # Guard: check if dnf lock is held (another process installing) local lock_retries=5 while fuser /var/lib/rpm/.rpm.lock >/dev/null 2>&1; do if [[ $lock_retries -le 0 ]]; then error_exit "dnf/rpm lock held by another process after 25 seconds. Try again later." fi warning "dnf/rpm lock detected, waiting 5s... ($lock_retries retries left)" sleep 5 lock_retries=$((lock_retries - 1)) done if ! dnf install -y redis 2>&1; then error_exit "dnf install redis failed. Check: (1) internet connectivity, (2) dnf repo config, (3) run: dnf install -y redis" fi # Verify binary appeared post-install if ! command -v redis-server >/dev/null 2>&1; then error_exit "dnf reported success but redis-server binary not found. Package may be broken." fi success "Redis package installed successfully." } # ── STEP 2: Configure redis.conf ───────────────────────────────────────────── step_configure() { local conf conf=$(get_redis_conf) if [[ -z "$conf" ]]; then error_exit "Cannot find redis.conf after installation. Checked: ${REDIS_CONF_PATHS[*]}. Run: rpm -ql redis | grep conf" fi info "Using Redis config: $conf" # Backup (idempotent — only on first run) if [[ ! -f "${conf}.orig" ]]; then cp "$conf" "${conf}.orig" || error_exit "Cannot backup $conf — check disk space and permissions." info "Backed up original config to ${conf}.orig" fi # Ensure socket directory exists with correct ownership BEFORE writing config if ! mkdir -p "$REDIS_SOCKET_DIR" 2>/dev/null; then error_exit "Cannot create socket directory $REDIS_SOCKET_DIR" fi # redis user must own the socket dir so it can create the socket file if ! chown redis:redis "$REDIS_SOCKET_DIR" 2>/dev/null; then warning "Could not chown $REDIS_SOCKET_DIR to redis:redis — socket creation may fail." fi chmod 755 "$REDIS_SOCKET_DIR" # bind: restrict to loopback only (security hardening) if grep -q '^bind ' "$conf"; then sed -i 's/^bind .*/bind 127.0.0.1/' "$conf" else echo "bind 127.0.0.1" >> "$conf" fi # unixsocket path if grep -q '^unixsocket ' "$conf"; then sed -i "s|^unixsocket .*|unixsocket $REDIS_SOCKET|" "$conf" else echo "unixsocket $REDIS_SOCKET" >> "$conf" fi # unixsocketperm: 775 so redis group members (litespeed) can r/w if grep -q '^unixsocketperm ' "$conf"; then sed -i 's/^unixsocketperm .*/unixsocketperm 775/' "$conf" else echo "unixsocketperm 775" >> "$conf" fi # maxmemory: only set if not already defined if ! grep -q '^maxmemory ' "$conf"; then echo "maxmemory 256mb" >> "$conf" echo "maxmemory-policy allkeys-lru" >> "$conf" info "Set maxmemory 256mb with allkeys-lru eviction policy." fi # Verify config was written (sanity check) if ! grep -q "^unixsocket $REDIS_SOCKET" "$conf"; then error_exit "Config write verification failed — unixsocket not found in $conf after editing." fi success "Redis configuration applied at $conf" } # ── STEP 3: Add litespeed to redis group ───────────────────────────────────── step_socket_access() { if ! id "$WEB_USER" >/dev/null 2>&1; then warning "User '$WEB_USER' does not exist on this system — skipping group membership." return 0 fi if ! getent group redis >/dev/null 2>&1; then warning "Group 'redis' does not exist yet — will retry after service start." return 0 fi if id -nG "$WEB_USER" | grep -qw redis; then info "$WEB_USER is already a member of the redis group." return 0 fi if usermod -aG redis "$WEB_USER"; then success "$WEB_USER added to redis group." else warning "usermod failed to add $WEB_USER to redis group. Socket access may be restricted." fi } # ── STEP 4: Enable + start Redis service ───────────────────────────────────── step_start() { local svc svc=$(get_redis_svc) if [[ -z "$svc" ]]; then error_exit "No Redis systemd service found after installation. Check: systemctl list-unit-files | grep redis" fi info "Enabling $svc at boot..." systemctl enable "$svc" 2>/dev/null || warning "Could not enable $svc at boot (non-fatal)." info "Starting $svc..." if ! systemctl restart "$svc" 2>/dev/null; then local journal journal=$(journalctl -u "$svc" -n 20 --no-pager 2>/dev/null || echo "unavailable") error_exit "Failed to start $svc. Last 20 journal lines:\n$journal" fi # Wait up to 15s for socket to appear local waited=0 while [[ $waited -lt 15 ]]; do if [[ -S "$REDIS_SOCKET" ]]; then break fi sleep 1 waited=$((waited + 1)) done if [[ ! -S "$REDIS_SOCKET" ]]; then local journal journal=$(journalctl -u "$svc" -n 20 --no-pager 2>/dev/null || echo "unavailable") error_exit "Redis started ($svc) but socket not found at $REDIS_SOCKET after 15s.\nJournal:\n$journal" fi # Final PING verification local ping_result ping_result=$(redis-cli -s "$REDIS_SOCKET" ping 2>/dev/null || echo "FAILED") if [[ "$ping_result" != "PONG" ]]; then error_exit "Socket exists at $REDIS_SOCKET but PING returned '$ping_result'. Redis may have crashed." fi success "Redis is running and healthy (service=$svc, socket=$REDIS_SOCKET, ping=PONG)" } # ── STEP 5: Re-check group membership after service starts (socket now exists) ── step_verify_access() { if ! id "$WEB_USER" >/dev/null 2>&1; then return 0 fi # Ensure redis group exists now (created by service start) if ! getent group redis >/dev/null 2>&1; then warning "redis group still not found after service start — skipping access check." return 0 fi if ! id -nG "$WEB_USER" | grep -qw redis; then info "Re-attempting to add $WEB_USER to redis group..." usermod -aG redis "$WEB_USER" || warning "Could not add $WEB_USER to redis group." fi # Verify socket is accessible by litespeed group local sock_perms sock_perms=$(stat -c '%A %G' "$REDIS_SOCKET" 2>/dev/null || echo "unknown") info "Socket permissions: $sock_perms" } # ── STEP 6: Configure WordPress Object Cache ────────────────────────────────── step_wp_object_cache() { local plugin_script="$SCRIPT_DIR/configure_litespeed_plugin_object_cache.sh" # Guard: script exists if [[ ! -f "$plugin_script" ]]; then warning "configure_litespeed_plugin_object_cache.sh not found at $plugin_script — skipping WP config." return 0 fi # Guard: WordPress present if [[ ! -f "$WP_ROOT/wp-config.php" ]]; then warning "WordPress not found at $WP_ROOT — skipping WP object cache config." return 0 fi # Guard: WP-CLI available if ! command -v wp >/dev/null 2>&1; then warning "WP-CLI not found — skipping WP object cache config. Install WP-CLI first." return 0 fi # Guard: LiteSpeed Cache plugin active (run as litespeed to get correct WP context) local plugin_status plugin_status=$(sudo -u "$WEB_USER" -s wp plugin is-active litespeed-cache --path="$WP_ROOT" 2>/dev/null && echo "active" || echo "inactive") if [[ "$plugin_status" != "active" ]]; then warning "LiteSpeed Cache plugin is not active in WordPress — skipping object cache config." warning "Activate it at: WordPress Admin > Plugins > LiteSpeed Cache" return 0 fi info "Configuring WordPress LiteSpeed Cache Plugin Object Cache..." if bash "$plugin_script" --enable --connection-type=socket --redis-socket="$REDIS_SOCKET" --wp-root="$WP_ROOT"; then success "WordPress Object Cache configured successfully with Redis socket." else warning "Object Cache config script failed (non-fatal). Run manually:" warning " bash $plugin_script --enable --wp-root=$WP_ROOT" fi } # ════════════════════════════════════════════════════════════════════════════ # MAIN # ════════════════════════════════════════════════════════════════════════════ # Must be root if [[ "$(id -u)" -ne 0 ]]; then error_exit "This script must be run as root." fi if [[ "$STATUS_ONLY" == "true" ]]; then do_status exit $? fi info "════════ Redis Install & Configure for WordPress (LLSMP/AlmaLinux) ════════" # Run status check first — if Redis is already healthy, nothing to do status_rc=0 do_status || status_rc=$? if [[ $status_rc -eq 0 ]]; then success "Redis is already installed and running. No action needed." info "To reconfigure WordPress object cache, run:" info " bash $SCRIPT_DIR/configure_litespeed_plugin_object_cache.sh --enable --wp-root=$WP_ROOT" exit 0 fi if [[ $status_rc -eq 2 ]]; then # Installed but stopped — skip install/configure, just start it info "Redis is installed but stopped. Attempting to start..." step_start step_verify_access step_wp_object_cache success "════════ Redis re-enabled successfully ════════" exit 0 fi # status_rc=1: not installed — full install flow info "Redis not installed. Starting full installation..." step_install step_configure step_socket_access step_start step_verify_access step_wp_object_cache success "════════ Redis setup complete ════════" info "Socket : $REDIS_SOCKET" info "Config : $(get_redis_conf)" info "Test : redis-cli -s $REDIS_SOCKET ping"