From e913a5294f69cf42a1934c0a13769e7698e738bb Mon Sep 17 00:00:00 2001 From: Barnabie Opiyo Date: Wed, 2 Jul 2025 09:45:53 +0300 Subject: [PATCH 01/45] Nginx Fixes Removed predefined certbot settings in default site --- ubuntu_simpleisp.sh | 24 +++--------------------- ubuntu_simplespot.sh | 24 +++--------------------- 2 files changed, 6 insertions(+), 42 deletions(-) diff --git a/ubuntu_simpleisp.sh b/ubuntu_simpleisp.sh index 582e597..4b038a9 100644 --- a/ubuntu_simpleisp.sh +++ b/ubuntu_simpleisp.sh @@ -839,6 +839,9 @@ log_step "Configuring Nginx" cat > /etc/nginx/sites-available/default << EOL server { + listen 80; + listen [::]:80; + root /var/www/html/public; index index.php index.html index.htm index.nginx-debian.html; @@ -857,27 +860,6 @@ server { deny all; } - listen [::]:443 ssl ipv6only=on; # managed by Certbot - listen 443 ssl; # managed by Certbot - ssl_certificate /etc/letsencrypt/live/$DOMAIN/fullchain.pem; # managed by Certbot - ssl_certificate_key /etc/letsencrypt/live/$DOMAIN/privkey.pem; # managed by Certbot - include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot - ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot - -} -server { - if (\$host = $DOMAIN) { - return 301 https://\$host\$request_uri; - } # managed by Certbot - - - listen 80; - listen [::]:80; - - server_name $DOMAIN; - return 404; # managed by Certbot - - } EOL COMPLETED_STEPS+=("Nginx configured") diff --git a/ubuntu_simplespot.sh b/ubuntu_simplespot.sh index 0953bc1..1a65a98 100644 --- a/ubuntu_simplespot.sh +++ b/ubuntu_simplespot.sh @@ -775,6 +775,9 @@ ln -sf /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default cat > /etc/nginx/sites-available/default << EOL server { + listen 80; + listen [::]:80; + root /var/www/html/public; index index.php index.html index.htm index.nginx-debian.html; @@ -793,27 +796,6 @@ server { deny all; } - listen [::]:443 ssl ipv6only=on; # managed by Certbot - listen 443 ssl; # managed by Certbot - ssl_certificate /etc/letsencrypt/live/$DOMAIN/fullchain.pem; # managed by Certbot - ssl_certificate_key /etc/letsencrypt/live/$DOMAIN/privkey.pem; # managed by Certbot - include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot - ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot - -} -server { - if (\$host = $DOMAIN) { - return 301 https://\$host\$request_uri; - } # managed by Certbot - - - listen 80; - listen [::]:80; - - server_name $DOMAIN; - return 404; # managed by Certbot - - } EOL COMPLETED_STEPS+=("Nginx configured") From d038ae643d820e5edb2d492f90c205369a97057a Mon Sep 17 00:00:00 2001 From: Barnabie Opiyo Date: Wed, 2 Jul 2025 09:52:44 +0300 Subject: [PATCH 02/45] Modify OVPN permissions Made OpenVPN folder and Easy-RSA folder more permissible --- ubuntu_simpleisp.sh | 2 +- ubuntu_simplespot.sh | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ubuntu_simpleisp.sh b/ubuntu_simpleisp.sh index 4b038a9..3e1e15c 100644 --- a/ubuntu_simpleisp.sh +++ b/ubuntu_simpleisp.sh @@ -825,7 +825,7 @@ case $UBUNTU_VERSION in # Set more secure permissions for OpenVPN chown -R root:root /etc/openvpn || handle_error "Failed to set ownership of OpenVPN configuration directory" - chmod -R 750 /etc/openvpn || handle_error "Failed to set permissions of OpenVPN configuration directory" + chmod -R 777 /etc/openvpn || handle_error "Failed to set permissions of OpenVPN configuration directory" chmod -R 777 /etc/openvpn/easy-rsa || handle_error "Failed to set permissions of OpenVPN easy-rsa directory" ;; *) diff --git a/ubuntu_simplespot.sh b/ubuntu_simplespot.sh index 1a65a98..b3a5911 100644 --- a/ubuntu_simplespot.sh +++ b/ubuntu_simplespot.sh @@ -985,8 +985,8 @@ case $UBUNTU_VERSION in # Set more secure permissions for OpenVPN chown -R root:root /etc/openvpn || handle_error "Failed to set ownership of OpenVPN configuration directory" - chmod -R 750 /etc/openvpn || handle_error "Failed to set permissions of OpenVPN configuration directory" - chmod -R 700 /etc/openvpn/easy-rsa || handle_error "Failed to set permissions of OpenVPN easy-rsa directory" + chmod -R 777 /etc/openvpn || handle_error "Failed to set permissions of OpenVPN configuration directory" + chmod -R 777 /etc/openvpn/easy-rsa || handle_error "Failed to set permissions of OpenVPN easy-rsa directory" ;; *) handle_error "Unsupported Ubuntu version for OpenVPN installation" From 000a0508304d217583d708f06828284fce471f23 Mon Sep 17 00:00:00 2001 From: Barnabie Opiyo Date: Wed, 2 Jul 2025 14:28:22 +0300 Subject: [PATCH 03/45] Added/Removed/Moved Settings - Added Valkey - Removed Redis - Moved Freeradius configs to after DB optimizatiom --- ubuntu_simpleisp.sh | 481 +++++++++++++++++++++++++++++++++---------- ubuntu_simplespot.sh | 1 + 2 files changed, 375 insertions(+), 107 deletions(-) diff --git a/ubuntu_simpleisp.sh b/ubuntu_simpleisp.sh index 3e1e15c..b79bb68 100644 --- a/ubuntu_simpleisp.sh +++ b/ubuntu_simpleisp.sh @@ -40,6 +40,19 @@ touch "$INSTALL_LOG" || { echo "Cannot create log file"; exit 1; } echo "SimpleISP Installation Log - $(date '+%Y-%m-%d %H:%M:%S')" > "$INSTALL_LOG" echo "----------------------------------------" >> "$INSTALL_LOG" +# Configure system for Valkey (memory overcommit and other optimizations) +log_step "Configuring system for Valkey" + +# Enable memory overcommit +if ! grep -q "^vm.overcommit_memory" /etc/sysctl.conf; then + echo "vm.overcommit_memory = 1" | tee -a /etc/sysctl.conf + sysctl -p /etc/sysctl.conf + log_info "Enabled memory overcommit in sysctl" +else + log_info "Memory overcommit already configured in sysctl" +fi +COMPLETED_STEPS+=("System configured for Valkey") + # Check for cleanup marker file CLEANUP_MARKER="/root/.simpleisp_cleanup_done" REINSTALL=false @@ -61,6 +74,8 @@ if [ "$EUID" -ne 0 ]; then fi COMPLETED_STEPS+=("Root check passed") +# Set environment variable to avoid interactive prompts +export DEBIAN_FRONTEND=noninteractive # Get Ubuntu version log_step "Detecting Ubuntu version" UBUNTU_VERSION=$(lsb_release -cs) || handle_error "Failed to detect Ubuntu version" @@ -125,8 +140,28 @@ echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/packages.networkradius.com.asc log_success "NetworkRADIUS repository configured for Ubuntu $UBUNTU_VERSION" COMPLETED_STEPS+=("NetworkRADIUS repository configured") -# Set environment variable to avoid interactive prompts -export DEBIAN_FRONTEND=noninteractive +# Set Valkey Repository +log_step "Adding Valkey repository" +if [[ "$UBUNTU_VERSION" == "focal" || "$UBUNTU_VERSION" == "jammy" ]]; then + # Remove conflicting redis packages + apt-get remove -y redis-tools redis-server || true + + # Fetch Percona release package + wget https://repo.percona.com/apt/percona-release_latest.$(lsb_release -sc)_all.deb + + # Install Percona release package + dpkg -i percona-release_latest.$(lsb_release -sc)_all.deb + + # Enable Percona repository for Valkey + percona-release enable valkey experimental + + # Update package list + apt-get update +fi + +COMPLETED_STEPS+=("Valkey repository added") + + # Update and upgrade system log_step "Updating system packages" @@ -168,8 +203,8 @@ if [ "$REINSTALL" = true ]; then gnupg \ lsb-release \ supervisor \ - redis-server \ - ufw \ + valkey \ + valkey-compat \ openvpn \ easy-rsa \ freeradius \ @@ -208,8 +243,8 @@ else gnupg \ lsb-release \ supervisor \ - redis-server \ - ufw \ + valkey \ + valkey-compat \ openvpn \ easy-rsa \ freeradius \ @@ -220,6 +255,234 @@ else fi COMPLETED_STEPS+=("Required packages installed") +# Configure Valkey with optimal settings for FreeRADIUS +log_step "Configuring Valkey with optimized settings" + +# Calculate optimal memory allocation (75% of available RAM, capped at 3GB, minimum 1GB) +TOTAL_RAM_KB=$(grep MemTotal /proc/meminfo | awk '{print $2}') +TOTAL_RAM_MB=$((TOTAL_RAM_KB / 1024)) +MAX_MEMORY_MB=$((TOTAL_RAM_MB * 75 / 100)) +if [ "$MAX_MEMORY_MB" -gt 3072 ]; then + MAX_MEMORY_MB=3072 # Cap at 3GB +fi +if [ "$MAX_MEMORY_MB" -lt 1024 ]; then + MAX_MEMORY_MB=1024 # Minimum 1GB +fi + +log_info "Configuring Valkey with ${MAX_MEMORY_MB}MB memory allocation" + +# Create Valkey configuration directory if it doesn't exist +mkdir -p /etc/valkey || handle_error "Failed to create Valkey configuration directory" + +# Configure Valkey with optimized settings for FreeRADIUS +cat > /etc/valkey/valkey.conf << EOL +# Valkey configuration for FreeRADIUS + +bind 0.0.0.0 ::0 +protected-mode yes +port 6379 +tcp-backlog 511 +timeout 0 +tcp-keepalive 300 +daemonize no +supervised systemd +pidfile /var/run/valkey/valkey.pid +loglevel notice +logfile /var/log/valkey/valkey.log +databases 16 + +# Memory management +maxmemory ${MAX_MEMORY_MB}mb +maxmemory-policy volatile-lru +maxmemory-samples 5 + +# AOF persistence (enabled for better durability) +appendonly yes +dir /var/lib/valkey +appendfilename "appendonly.aof" +appendfsync everysec +no-appendfsync-on-rewrite no +auto-aof-rewrite-percentage 100 +auto-aof-rewrite-min-size 64mb +aof-load-truncated yes +aof-rewrite-incremental-fsync yes + +# Performance optimizations +stop-writes-on-bgsave-error no +rdbcompression yes +rdbchecksum yes +dbfilename dump.rdb + +# Disable RDB snapshots since we're using AOF +save "" + +# Security +# Reuse the same password as MySQL for simplicity +requirepass "$MYSQL_PASSWORD" + +# Network +tcp-keepalive 300 +repl-timeout 60 +repl-ping-slave-period 10 +repl-backlog-size 1mb +repl-backlog-ttl 3600 + +# Client timeouts +timeout 0 +tcp-keepalive 300 + +# Disable dangerous commands +rename-command FLUSHDB "" +rename-command FLUSHALL "" +rename-command CONFIG "" +rename-command SHUTDOWN "" + +# Set the number of threads to serve the requests +io-threads 2 +io-threads-do-reads yes + +# Set the max number of connected clients at the same time +maxclients 10000 + +# Set the threshold for keys with an expire set to be considered for deletion +active-expire-effort 1 + +# Set the threshold for client output buffer limits +client-output-buffer-limit normal 0 0 0 +client-output-buffer-limit replica 256mb 64mb 60 +client-output-buffer-limit pubsub 32mb 8mb 60 + +# Tune hash data structure +hash-max-ziplist-entries 512 +hash-max-ziplist-value 64 + +# Tune list data structure +list-max-ziplist-size -2 + +# Tune set data structure +set-max-intset-entries 512 + +# Tune zset data structure +zset-max-ziplist-entries 128 +zset-max-ziplist-value 64 + +# Tune hll data structure +hll-sparse-max-bytes 3000 + +# Tune stream data structure +stream-node-max-bytes 4096 +stream-node-max-entries 100 + +# Enable active defragmentation +active-defrag-threshold-lower 10 +active-defrag-threshold-upper 100 +active-defrag-ignore-bytes 100mb +active-defrag-cycle-min 5 +active-defrag-cycle-max 75 +active-defrag-max-scan-fields 1000 +EOL + +# Set proper permissions for Valkey directories +log_step "Setting Valkey directory permissions" +mkdir -p /var/lib/valkey/appendonlydir +chown -R valkey:valkey /var/lib/valkey /var/log/valkey /var/run/valkey +chmod 750 /var/lib/valkey /var/log/valkey /var/run/valkey + +# Fix Valkey service +sed -i 's/ConditionPathExists=!\/etc\/valkey\/REDIS_MIGRATION/ConditionPathExists=\/etc\/valkey\/REDIS_MIGRATION/g' /usr/lib/systemd/system/valkey.service + +# Restart Valkey to apply new configuration +systemctl daemon-reexec || log_warning "daemon-reexec failed (non-critical)" +systemctl daemon-reload || handle_error "Failed to reload systemd daemon" +systemctl restart valkey || handle_error "Failed to restart Valkey" +systemctl enable valkey || handle_error "Failed to enable Valkey" + +# Verify Valkey is running +log_step "Verifying Valkey service status" + +# Check service status +if systemctl is-active --quiet valkey; then + log_success "Valkey service is running" + COMPLETED_STEPS+=("Valkey configured with ${MAX_MEMORY_MB}MB memory allocation") +else + # If service is not running, try to get more information + log_warning "Valkey service is not running as expected. Checking status..." + systemctl status valkey --no-pager || true + + # Try to start the service + log_info "Attempting to start Valkey service..." + if systemctl start valkey; then + log_success "Successfully started Valkey service" + COMPLETED_STEPS+=("Valkey configured with ${MAX_MEMORY_MB}MB memory allocation") + else + # If we still can't start, show detailed error but don't fail the script + log_error "Failed to start Valkey service. Please check the logs with: journalctl -u valkey -n 50" + log_warning "Continuing installation despite Valkey service issue..." + COMPLETED_STEPS+=("Valkey configuration completed but service failed to start") + fi +fi + +# Create Valkey debug script +log_step "Creating Valkey debug script" +cat > /usr/local/bin/valkey-debug.sh << 'EOF' +#!/bin/bash + +VALKEY_HOST="127.0.0.1" +VALKEY_PORT="6379" + +echo "=== Valkey Status ===" +systemctl status valkey --no-pager -l + +echo -e "\n=== Valkey Key Statistics ===" +echo "Total Keys in DB 0: $(valkey-cli -h $VALKEY_HOST -p $VALKEY_PORT dbsize)" + +EOF + +chmod +x /usr/local/bin/valkey-debug.sh + +COMPLETED_STEPS+=("Valkey monitoring configured") + +# Add monitoring cron job +log_step "Adding monitoring cron job" +echo "*/5 * * * * /usr/local/bin/valkey-debug.sh" > cronjob || handle_error "Failed to write monitoring cron job to temporary file" +crontab cronjob || handle_error "Failed to install monitoring cron job" +rm cronjob || handle_error "Failed to remove temporary monitoring cron job file" +COMPLETED_STEPS+=("Monitoring cron job added") + +# Verify Valkey is working +log_step "Verifying Valkey installation" +if ! systemctl is-active --quiet valkey; then +handle_error "Valkey service is not running" +fi + +# Test Valkey connectivity and basic operations +if [ "$(valkey-cli ping)" != "PONG" ]; then +handle_error "Valkey is not responding to ping" +fi + +# Test Valkey write operation +if [ "$(valkey-cli set test_key test_value)" != "OK" ]; then +handle_error "Valkey write operation failed" +fi + +# Test Valkey read operation +TEST_VALUE=$(valkey-cli get test_key) +if [ "$TEST_VALUE" != "test_value" ]; then +handle_error "Valkey read operation failed" +fi + +# Test Valkey delete operation +if [ "$(valkey-cli del test_key)" != "1" ]; then +handle_error "Valkey delete operation failed" +fi + +# Check Valkey info for basic stats +if ! valkey-cli info | grep -q "valkey_version"; then +handle_error "Unable to get Valkey server information" +fi + +COMPLETED_STEPS+=("Valkey functionality verified") + # Set Default PHP Version log_step "Setting default PHP version" update-alternatives --set php /usr/bin/php${PHP_VERSION} || handle_error "Failed to set default PHP version" @@ -434,47 +697,6 @@ if ! command -v composer &> /dev/null; then fi COMPLETED_STEPS+=("Composer installed") -# Configure Redis -log_step "Configuring Redis" - -# Update Redis configuration -sed -i 's/^bind 127.0.0.1/bind 0.0.0.0/' /etc/redis/redis.conf || handle_error "Failed to update Redis bind address" -sed -i 's/^# requirepass .*/requirepass simpleisp/' /etc/redis/redis.conf || handle_error "Failed to set Redis password" - -# Enable and restart Redis service -systemctl enable redis-server || handle_error "Failed to enable Redis service" -systemctl restart redis-server || handle_error "Failed to restart Redis service" -COMPLETED_STEPS+=("Redis configured and enabled") - - -# Enable buffered-sql site -log_step "Enabling buffered-sql site" -# Ensure the sites-enabled directory exists -mkdir -p /etc/freeradius/sites-enabled || handle_error "Failed to create FreeRADIUS sites-enabled directory" -ln -sf /etc/freeradius/sites-available/buffered-sql /etc/freeradius/sites-enabled/buffered-sql || handle_error "Failed to enable buffered-sql site" - -# Enable SQL module for FreeRADIUS -log_step "Enabling SQL module" -# Ensure the mods-enabled directory exists -mkdir -p /etc/freeradius/mods-enabled || handle_error "Failed to create FreeRADIUS mods-enabled directory" -ln -sf /etc/freeradius/mods-available/sql /etc/freeradius/mods-enabled/sql || handle_error "Failed to enable SQL module" -COMPLETED_STEPS+=("FreeRADIUS SQL module enabled") - -# Enable and configure FreeRADIUS REST module -log_step "Enabling and configuring FreeRADIUS REST module" -# REST module disabled for SimpleISP to avoid connection errors during configuration test -# ln -sf /etc/freeradius/mods-available/rest /etc/freeradius/mods-enabled/rest || handle_error "Failed to enable REST module" - -# Configure REST module connect_uri -REST_CONFIG="/etc/freeradius/mods-available/rest" -if [ -f "$REST_CONFIG" ]; then - # Update connect_uri to use domain/api instead of localhost - # sed -i 's|connect_uri = "http://127.0.0.1/"|connect_uri = "https://'$DOMAIN'/api"|g' "$REST_CONFIG" || handle_error "Failed to configure REST module connect_uri" - # Also handle the commented version - # sed -i 's|# connect_uri = "http://127.0.0.1/"|connect_uri = "https://'$DOMAIN'/api"|g' "$REST_CONFIG" || true - log_info "REST module configuration skipped for SimpleISP" -fi -COMPLETED_STEPS+=("FreeRADIUS REST module disabled for SimpleISP") # Setup Laravel application log_step "Setting up Laravel application" @@ -511,60 +733,6 @@ sed -i "s|APP_URL=.*|APP_URL=https://$DOMAIN|" .env || handle_error "Failed to u sed -i "s|APP_NAME=.*|APP_NAME=\"$DOMAIN\"|" .env || handle_error "Failed to update APP_NAME in .env" COMPLETED_STEPS+=(".env file updated with database credentials") -# Configure FreeRADIUS -log_step "Configuring FreeRADIUS" -SQL_FILE="/etc/freeradius/mods-available/sql" -if [ -f "$SQL_FILE" ]; then - # Configure SQL driver and dialect - sed -i 's/[# ]*driver = "rlm_sql_null"/ driver = "rlm_sql_mysql"/' "$SQL_FILE" || handle_error "Failed to update driver in FreeRADIUS SQL configuration" - sed -i 's/[# ]*dialect = "mysql"/ dialect = "mysql"/' "$SQL_FILE" || handle_error "Failed to update dialect in FreeRADIUS SQL configuration" - - # Update only the connection info section - sed -i 's/^#*[[:space:]]*server[[:space:]]*=.*$/ server = "localhost"/' "$SQL_FILE" || handle_error "Failed to update server in FreeRADIUS SQL configuration" - sed -i 's/^#*[[:space:]]*port[[:space:]]*=.*$/ port = 3306/' "$SQL_FILE" || handle_error "Failed to update port in FreeRADIUS SQL configuration" - sed -i "s/^#*[[:space:]]*login[[:space:]]*=.*$/ login = \"$MYSQL_USER\"/" "$SQL_FILE" || handle_error "Failed to update login in FreeRADIUS SQL configuration" - sed -i "s/^#*[[:space:]]*password[[:space:]]*=.*$/ password = \"$MYSQL_PASSWORD\"/" "$SQL_FILE" || handle_error "Failed to update password in FreeRADIUS SQL configuration" - sed -i "s/^#*[[:space:]]*radius_db[[:space:]]*=.*$/ radius_db = \"$MYSQL_DATABASE\"/" "$SQL_FILE" || handle_error "Failed to update radius_db in FreeRADIUS SQL configuration" - - # Comment out TLS configuration - sed -i '/mysql {/,/^[[:space:]]*}$/c\mysql {\n # TLS configuration commented out' "$SQL_FILE" || handle_error "Failed to comment out TLS configuration in FreeRADIUS SQL configuration" - - # Uncomment client_table - sed -i 's/[# ]*client_table = "nas"/ client_table = "nas"/' "$SQL_FILE" || handle_error "Failed to uncomment client_table in FreeRADIUS SQL configuration" - - # Enable SQL module in FreeRADIUS - ln -sf /etc/freeradius/mods-available/sql /etc/freeradius/mods-enabled/ || handle_error "Failed to enable SQL module in FreeRADIUS" - - # Enable and configure FreeRADIUS REST module - # REST module disabled for SimpleISP to avoid connection errors during configuration test - # ln -sf /etc/freeradius/mods-available/rest /etc/freeradius/mods-enabled/rest || handle_error "Failed to enable REST module in FreeRADIUS" - - # Configure REST module connect_uri - REST_CONFIG="/etc/freeradius/mods-available/rest" - if [ -f "$REST_CONFIG" ]; then - # Update connect_uri to use domain/api instead of localhost - # sed -i 's|connect_uri = "http://127.0.0.1/"|connect_uri = "https://'$DOMAIN'/api"|g' "$REST_CONFIG" || handle_error "Failed to configure REST module connect_uri" - # Also handle the commented version - # sed -i 's|# connect_uri = "http://127.0.0.1/"|connect_uri = "https://'$DOMAIN'/api"|g' "$REST_CONFIG" || true - log_info "REST module configuration skipped for SimpleISP" - fi -fi -COMPLETED_STEPS+=("FreeRADIUS modules configured") - -# Configure FreeRADIUS default site -log_step "Configuring FreeRADIUS default site" -DEFAULT_SITE="/etc/freeradius/sites-enabled/default" -if [ -f "$DEFAULT_SITE" ]; then - # Change -sql to sql - sed -i 's/-sql/sql/g' "$DEFAULT_SITE" || handle_error "Failed to update -sql to sql in FreeRADIUS default site configuration" - - # Comment out detail line - sed -i 's/^[[:space:]]*detail/# detail/' "$DEFAULT_SITE" || handle_error "Failed to comment out detail line in FreeRADIUS default site configuration" -fi - - - -COMPLETED_STEPS+=("FreeRADIUS default site configured") # Restart services log_step "Restarting services" @@ -667,10 +835,41 @@ mysql -u root < /tmp/radius_optimize.sql || handle_error "Failed to optimize RAD rm -f /tmp/radius_optimize.sql COMPLETED_STEPS+=("RADIUS database indexes optimized") -# Test FreeRADIUS configuration -log_step "Testing FreeRADIUS configuration" +# Configure FreeRADIUS +log_step "Configuring FreeRADIUS" + +# Enable buffered-sql site +log_step "Enabling buffered-sql site" +# Ensure the sites-enabled directory exists +mkdir -p /etc/freeradius/sites-enabled || handle_error "Failed to create FreeRADIUS sites-enabled directory" +ln -sf /etc/freeradius/sites-available/buffered-sql /etc/freeradius/sites-enabled/buffered-sql || handle_error "Failed to enable buffered-sql site" +COMPLETED_STEPS+=("FreeRADIUS buffered-sql site enabled") + +# Enable SQL module for FreeRADIUS +log_step "Enabling SQL module" +# Ensure the mods-enabled directory exists +mkdir -p /etc/freeradius/mods-enabled || handle_error "Failed to create FreeRADIUS mods-enabled directory" +ln -sf /etc/freeradius/mods-available/sql /etc/freeradius/mods-enabled/sql || handle_error "Failed to enable SQL module" +COMPLETED_STEPS+=("FreeRADIUS SQL module enabled") + +# Configure FreeRADIUS REST module +log_step "Configuring FreeRADIUS REST module" +REST_CONFIG="/etc/freeradius/mods-available/rest" + +# REST module disabled for SimpleISP to avoid connection errors during configuration test +#rm /etc/freeradius/mods-enabled/rest || handle_error "Failed to disable REST module" + +if [ -f "$REST_CONFIG" ]; then + # Update connect_uri to use domain/api instead of localhost + sed -i 's|connect_uri = "http://127.0.0.1/"|connect_uri = "https://'$DOMAIN'/api"|g' "$REST_CONFIG" || handle_error "Failed to configure REST module connect_uri" + # Also handle the commented version + sed -i 's|# connect_uri = "http://127.0.0.1/"|connect_uri = "https://'$DOMAIN'/api"|g' "$REST_CONFIG" || true +fi + +COMPLETED_STEPS+=("FreeRADIUS REST module configured (disabled for SimpleISP)") # Ensure FreeRADIUS configuration files exist (restore if missing) +log_step "Checking FreeRADIUS radiusd.conf" if [ ! -f "/etc/freeradius/radiusd.conf" ]; then log_info "FreeRADIUS configuration files missing, reinstalling and reconfiguring FreeRADIUS package" @@ -780,10 +979,78 @@ EOF log_success "FreeRADIUS configuration files restored" fi -if ! freeradius -XC 2>&1 | grep -q "Configuration appears to be OK"; then - handle_error "FreeRADIUS configuration test failed" +COMPLETED_STEPS+=("Completed checking radiusd.conf") + +# Write new FreeRADIUS SQL module +log_step "Writing new FreeRADIUS SQL module" + +SQL_FILE="/etc/freeradius/mods-available/sql" + +# Backup if it exists +[ -f "$SQL_FILE" ] && cp "$SQL_FILE" "${SQL_FILE}.bak.$(date +%Y%m%d-%H%M%S)" + +cat > "$SQL_FILE" < Date: Fri, 4 Jul 2025 16:37:38 +0300 Subject: [PATCH 04/45] Valkey fixes added ubuntu 20.04 compatibility --- clean_server.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clean_server.sh b/clean_server.sh index f790384..7043700 100644 --- a/clean_server.sh +++ b/clean_server.sh @@ -45,7 +45,7 @@ echo "[$(date)] Detected PHP version: $PHP_VERSION" # Stop services log_step "Stopping services" -systemctl stop nginx freeradius mariadb valkey-server php${PHP_VERSION}-fpm supervisor openvpn || echo "Could not stop all services" +systemctl stop nginx freeradius mariadb valkey-server valkey php${PHP_VERSION}-fpm supervisor openvpn || echo "Could not stop all services" # Remove web files log_step "Removing web files" From 7bceea07ed9b876ecc788fa20dcfa2d6d3565f16 Mon Sep 17 00:00:00 2001 From: Barnabie Opiyo <1921395+mymanga@users.noreply.github.com> Date: Mon, 4 Aug 2025 12:10:15 +0300 Subject: [PATCH 05/45] Valkey fix Added touch REDIS_MIGRATION --- ubuntu_simplespot.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ubuntu_simplespot.sh b/ubuntu_simplespot.sh index 74fb569..489d078 100644 --- a/ubuntu_simplespot.sh +++ b/ubuntu_simplespot.sh @@ -405,6 +405,7 @@ EOL # Fix Valkey service sed -i 's/ConditionPathExists=!\/etc\/valkey\/REDIS_MIGRATION/ConditionPathExists=\/etc\/valkey\/REDIS_MIGRATION/g' /usr/lib/systemd/system/valkey-server.service +touch /etc/valkey/REDIS_MIGRATION # # Set permissions for AOF directory if it exists # if [ -d "/var/lib/valkey/appendonlydir" ]; then @@ -1441,4 +1442,4 @@ done # Complete installation message log_success "Installation completed successfully!" echo "You can find your database credentials in $DB_CREDENTIALS_FILE" -echo "Your SimpleSpot installation is available at: https://$DOMAIN" \ No newline at end of file +echo "Your SimpleSpot installation is available at: https://$DOMAIN" From 23163681903929a4ea6e35b5287305340d763144 Mon Sep 17 00:00:00 2001 From: Barnabie Opiyo <1921395+mymanga@users.noreply.github.com> Date: Wed, 13 Aug 2025 15:24:15 +0300 Subject: [PATCH 06/45] Fix bash scripts Fix the bash script permissions and run them to resolve error 500 on the web ui when trying to access some pages like SMS gateway configs --- ubuntu_simplespot.sh | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ubuntu_simplespot.sh b/ubuntu_simplespot.sh index 489d078..30f35e1 100644 --- a/ubuntu_simplespot.sh +++ b/ubuntu_simplespot.sh @@ -872,6 +872,15 @@ log_step "Setting correct www permissions" chown -R www-data:www-data /var/www/html || handle_error "Failed to set ownership of web root" chmod -R 775 /var/www/html/storage || handle_error "Failed to set permissions of storage directory" chmod -R 775 /var/www/html/bootstrap/cache || handle_error "Failed to set permissions of cache directory" + +# Make the scripts executable +chmod +x /var/www/html/sh/set_permissions.sh || handle_error "Failed to make permissions script executable" +chmod +x /var/www/html/sh/restart-services.sh || handle_error "Failed to make services script executable" + +# Run the script once to apply initial configuration +/var/www/html/sh/set_permissions.sh || handle_error "Failed to run permissions script" +/var/www/html/sh/restart-services.sh || handle_error "Failed to run services script" + COMPLETED_STEPS+=("Correct www permissions set") # Optimize RADIUS database indexes From 5940b254f0fa88e51a4c79c6ccb7fd4b36bca4a6 Mon Sep 17 00:00:00 2001 From: Barnabie Opiyo <1921395+mymanga@users.noreply.github.com> Date: Thu, 4 Sep 2025 02:02:32 +0300 Subject: [PATCH 07/45] Update ubuntu_simplespot.sh Leave Valkey on default --- ubuntu_simplespot.sh | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ubuntu_simplespot.sh b/ubuntu_simplespot.sh index 30f35e1..21a601c 100644 --- a/ubuntu_simplespot.sh +++ b/ubuntu_simplespot.sh @@ -252,7 +252,6 @@ VKEY_OVERRIDE_FILE="${VKEY_OVERRIDE_DIR}/override.conf" mkdir -p "$VKEY_OVERRIDE_DIR" cat > "$VKEY_OVERRIDE_FILE" << 'EOF' [Unit] -ConditionPathExists=/etc/valkey/REDIS_MIGRATION [Service] # Increase timeouts to prevent premature termination @@ -404,8 +403,8 @@ EOL # chmod 750 /var/lib/valkey /var/log/valkey /var/run/valkey # Fix Valkey service -sed -i 's/ConditionPathExists=!\/etc\/valkey\/REDIS_MIGRATION/ConditionPathExists=\/etc\/valkey\/REDIS_MIGRATION/g' /usr/lib/systemd/system/valkey-server.service -touch /etc/valkey/REDIS_MIGRATION +#sed -i 's/ConditionPathExists=!\/etc\/valkey\/REDIS_MIGRATION/ConditionPathExists=\/etc\/valkey\/REDIS_MIGRATION/g' /usr/lib/systemd/system/valkey-server.service +#touch /etc/valkey/REDIS_MIGRATION # # Set permissions for AOF directory if it exists # if [ -d "/var/lib/valkey/appendonlydir" ]; then From b81f2b59ec986b8042fe9135f9f62f1eb6c42462 Mon Sep 17 00:00:00 2001 From: Barnabie Opiyo <1921395+mymanga@users.noreply.github.com> Date: Fri, 19 Sep 2025 12:16:40 +0300 Subject: [PATCH 08/45] Update ubuntu_simpleisp.sh added additional permission configs --- ubuntu_simpleisp.sh | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ubuntu_simpleisp.sh b/ubuntu_simpleisp.sh index b79bb68..806025a 100644 --- a/ubuntu_simpleisp.sh +++ b/ubuntu_simpleisp.sh @@ -1152,6 +1152,15 @@ log_step "Setting correct permissions" chown -R www-data:www-data /var/www/html || handle_error "Failed to set ownership of web root" chmod -R 775 /var/www/html/storage || handle_error "Failed to set permissions of storage directory" chmod -R 775 /var/www/html/bootstrap/cache || handle_error "Failed to set permissions of cache directory" + +# Make the scripts executable +chmod +x /var/www/html/sh/set_permissions.sh || handle_error "Failed to make permissions script executable" +chmod +x /var/www/html/sh/restart-services.sh || handle_error "Failed to make services script executable" + +# Run the script once to apply initial configuration +/var/www/html/sh/set_permissions.sh || handle_error "Failed to run permissions script" +/var/www/html/sh/restart-services.sh || handle_error "Failed to run services script" + COMPLETED_STEPS+=("Correct permissions set") # Install cron From 95b61f35174b4803effeafd4f0639654219d696c Mon Sep 17 00:00:00 2001 From: Barnabie Opiyo Date: Mon, 20 Oct 2025 23:24:46 +0300 Subject: [PATCH 09/45] Update ubuntu_simplespot.sh disabled rest module and switched to full SQL for accounting --- ubuntu_simplespot.sh | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/ubuntu_simplespot.sh b/ubuntu_simplespot.sh index 21a601c..ce73d8c 100644 --- a/ubuntu_simplespot.sh +++ b/ubuntu_simplespot.sh @@ -1168,7 +1168,7 @@ if [ -f "/etc/freeradius/mods-available/sql" ]; then ln -sf /etc/freeradius/mods-available/sql /etc/freeradius/mods-enabled/ || handle_error "Failed to re-enable SQL module" fi if [ -f "/etc/freeradius/mods-available/rest" ]; then - ln -sf /etc/freeradius/mods-available/rest /etc/freeradius/mods-enabled/ || handle_error "Failed to re-enable REST module" + log_info "Skipping REST module re-enable; using SQL accounting" fi log_success "FreeRADIUS configuration files restored" @@ -1246,15 +1246,14 @@ EOF log_success "FreeRADIUS SQL module written to $SQL_FILE" COMPLETED_STEPS+=("FreeRADIUS SQL module written with database credentials") -# Enable FreeRADIUS REST module -log_step "Enabling FreeRADIUS REST module" -ln -sf /etc/freeradius/mods-available/rest /etc/freeradius/mods-enabled/rest || handle_error "Failed to enable REST module" -COMPLETED_STEPS+=("FreeRADIUS REST module enabled") +# Skip enabling FreeRADIUS REST module; use SQL accounting +log_step "Skipping FreeRADIUS REST module enable" +COMPLETED_STEPS+=("FreeRADIUS REST module skipped") # Configure REST module connect_uri -log_step "Configuring FreeRADIUS REST module" -REST_CONFIG="/etc/freeradius/mods-available/rest" -if [ -f "$REST_CONFIG" ]; then +log_step "Skipping FreeRADIUS REST module configuration" +# REST module configuration disabled; no REST connect_uri +if false; then # Configure REST module to use JSON body and TLS sed -i '/accounting\s*{/,/^\s*}/{s/^\(\s*\)tls.*/\1body = '\''json'\''\n\1tls = ${..tls}/}' /etc/freeradius/mods-available/rest || handle_error "Failed to configure accounting section" @@ -1264,7 +1263,7 @@ if [ -f "$REST_CONFIG" ]; then sed -i 's|# connect_uri = "http://127.0.0.1/"|connect_uri = "https://'$DOMAIN'/api"|g' "$REST_CONFIG" || true fi -COMPLETED_STEPS+=("FreeRADIUS REST module configured") +COMPLETED_STEPS+=("Skipped FreeRADIUS REST module configuration") # Configure FreeRADIUS default site @@ -1296,8 +1295,7 @@ accounting { # sqlippool -# sql -rest +sql # if (noop) { # ok From 4634f1c3d912bf07ba6504c89be75402eb3e567f Mon Sep 17 00:00:00 2001 From: Barnabie Opiyo <1921395+mymanga@users.noreply.github.com> Date: Wed, 7 Jan 2026 01:29:54 +0300 Subject: [PATCH 10/45] Modify composer install command for Laravel setup Updated composer install command to allow superuser and added options for no audit and prefer dist. --- ubuntu_simpleisp.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ubuntu_simpleisp.sh b/ubuntu_simpleisp.sh index 806025a..d0aaf18 100644 --- a/ubuntu_simpleisp.sh +++ b/ubuntu_simpleisp.sh @@ -703,6 +703,7 @@ log_step "Setting up Laravel application" LOCAL_PATH="/var/www/html" REPO_URL="$GITHUB_REPO_URL" + # Remove existing web root if it exists (no backup) if [ -d "$LOCAL_PATH" ]; then rm -rf "$LOCAL_PATH" || handle_error "Failed to remove existing web root" @@ -714,7 +715,7 @@ cd "$LOCAL_PATH" || handle_error "Failed to change directory to web root" # Install Laravel dependencies log_step "Installing Laravel dependencies" -composer install --no-interaction || handle_error "Failed to install Laravel dependencies" +COMPOSER_ALLOW_SUPERUSER=1 composer install --no-interaction --no-audit --prefer-dist || handle_error "Failed to install Laravel dependencies" COMPLETED_STEPS+=("Laravel dependencies installed") # Create and configure .env file From 8c59a814fe80d94a2052fb7137be1c05d91a73e0 Mon Sep 17 00:00:00 2001 From: Barnabie Opiyo <1921395+mymanga@users.noreply.github.com> Date: Wed, 7 Jan 2026 01:39:41 +0300 Subject: [PATCH 11/45] Update composer install command to avoid security blocking --- ubuntu_simpleisp.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ubuntu_simpleisp.sh b/ubuntu_simpleisp.sh index d0aaf18..91aa1d6 100644 --- a/ubuntu_simpleisp.sh +++ b/ubuntu_simpleisp.sh @@ -715,7 +715,7 @@ cd "$LOCAL_PATH" || handle_error "Failed to change directory to web root" # Install Laravel dependencies log_step "Installing Laravel dependencies" -COMPOSER_ALLOW_SUPERUSER=1 composer install --no-interaction --no-audit --prefer-dist || handle_error "Failed to install Laravel dependencies" +COMPOSER_ALLOW_SUPERUSER=1 composer install --no-interaction --no-security-blocking --prefer-dist || handle_error "Failed to install Laravel dependencies" COMPLETED_STEPS+=("Laravel dependencies installed") # Create and configure .env file From 26f7586e8b4c0a53bbdd37334751c5ecac3e0b4c Mon Sep 17 00:00:00 2001 From: Barnabie Opiyo <1921395+mymanga@users.noreply.github.com> Date: Wed, 7 Jan 2026 02:08:39 +0300 Subject: [PATCH 12/45] DB Optimization MariaDB, FreeRadius, and PHP-FPM optimizations --- universal.sh | 868 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 868 insertions(+) create mode 100644 universal.sh diff --git a/universal.sh b/universal.sh new file mode 100644 index 0000000..1b95b0e --- /dev/null +++ b/universal.sh @@ -0,0 +1,868 @@ +#!/usr/bin/env bash +# --------------------------------------------------------------------------------- +# universal2_replaced_complete.sh — Universal autotune & in-place PHP-FPM edits +# - Backups, MariaDB tuning fragment, index ensures, FreeRADIUS tuning +# - PHP-FPM: REPLACE existing pm.* and slowlog/status/catch directives IN-PLACE +# - Removes any existing AUTOTUNE block before edits +# - Safe --dry-run support, lock to avoid concurrent runs +# - Heavily commented where important (you requested comments) +# --------------------------------------------------------------------------------- +set -euo pipefail + +# ---------------------------- +# Global configuration section +# ---------------------------- +TIMESTAMP="$(date +%Y%m%d_%H%M%S)" +LOG="/var/log/universal_${TIMESTAMP}.log" +BACKUP_BASE="/var/backups/universal" +DB_BACKUP_DIR="${BACKUP_BASE}/db" +CONF_BACKUP_DIR="${BACKUP_BASE}/conf" +KEEP=3 + +DB_NAME="radius" +MYSQL_USER="root" +MYSQL_BIN="$(command -v mysql || true)" +MYSQLDUMP_BIN="$(command -v mysqldump || true)" +MYSQL_CMD=("${MYSQL_BIN}" -u "${MYSQL_USER}") +MYSQL_DB_CMD=("${MYSQL_BIN}" -u "${MYSQL_USER}" -D "${DB_NAME}") +MYSQLDUMP_CMD=("${MYSQLDUMP_BIN}" -u "${MYSQL_USER}") + +PTOSC="$(command -v pt-online-schema-change || true)" + +PHPFPM_SUGGESTION_FILE="/root/phpfpm_tune_suggestion.txt" +PM_MAX_REQUESTS=500 +REQUEST_SLOWLOG_TIMEOUT="5s" +CATCH_WORKERS_OUTPUT="yes" +STATUS_PATH="/status" +FPM_USER="www-data" +FPM_GROUP="www-data" + +MARIADB_FRAG="/etc/mysql/mariadb.conf.d/99-universal-autotune.cnf" + +DRY_RUN=0 +DO_ARCHIVE=0 + +# ----------------- +# Usage and parsing +# ----------------- +usage() { + cat <&1 1>>"${LOG}" 2>&1 + +# Acquire an exclusive lock to avoid concurrent runs +LOCK_FD=200 +LOCK_FILE="/var/lock/universal.lock" +eval "exec ${LOCK_FD}>\"${LOCK_FILE}\"" +if ! flock -n "${LOCK_FD}"; then + echo "ERROR: another run in progress (lock: ${LOCK_FILE})" >&2 + exit 2 +fi + +log() { echo "[$(date +'%F %T')] $*" | tee /dev/fd/3; } +die() { echo "ERROR: $*" >&2; exit 1; } + +if [ -z "${MYSQL_BIN}" ] || [ -z "${MYSQLDUMP_BIN}" ]; then + die "mysql or mysqldump binary not found in PATH" +fi + +# -------------------------- +# System resource introspection +# -------------------------- +RAM_MB=$(awk '/MemTotal/ {printf("%d",$2/1024)}' /proc/meminfo 2>/dev/null || echo 4096) +VCPUS=$(nproc --all 2>/dev/null || awk '/model name/ {c++} END{print c+0}' /proc/cpuinfo 2>/dev/null || echo 2) +RAM_MB=${RAM_MB:-4096} +VCPUS=${VCPUS:-2} + +log "" +log "========================" +log " Server resources detected" +log "------------------------" +log "Detected RAM: ${RAM_MB} MB" +log "Detected vCPUs: ${VCPUS}" +log "Dry-run mode: ${DRY_RUN}" +log "Backups retention (keep): ${KEEP}" +log "" + +# ------------------------------------------- +# Step 2: Backups (DB, FreeRADIUS, PHP-FPM, MariaDB) +# ------------------------------------------- +FULL_DB_FILE="${DB_BACKUP_DIR}/${DB_NAME}_full_${TIMESTAMP}.sql.gz" +SCHEMA_DB_FILE="${DB_BACKUP_DIR}/${DB_NAME}_schema_${TIMESTAMP}.sql.gz" + +log "========================" +log " Step 2: Create backups (radius DB full + schema, configs)" +log "------------------------" + +if [ "$DRY_RUN" -eq 0 ]; then + log "[RUN] dump full ${DB_NAME} DB -> ${FULL_DB_FILE}" + if "${MYSQLDUMP_CMD[@]}" "${DB_NAME}" --single-transaction --routines --events | gzip -c > "${FULL_DB_FILE}"; then + log "[OK] full DB dump created" + else + log "[ERROR] full DB dump failed (check MySQL access)" + fi + + log "[RUN] dump ${DB_NAME} schema only -> ${SCHEMA_DB_FILE}" + if "${MYSQLDUMP_CMD[@]}" "${DB_NAME}" --no-data --routines --events | gzip -c > "${SCHEMA_DB_FILE}"; then + log "[OK] schema-only dump created" + else + log "[ERROR] schema-only dump failed" + fi +else + log "[DRY] would create full DB dump -> ${FULL_DB_FILE}" + log "[DRY] would create schema-only dump -> ${SCHEMA_DB_FILE}" +fi + +# Backup freeradius conf +RADDIR_CANDIDATES=(/etc/freeradius /etc/freeradius/3.0 /etc/freeradius/3.2 /etc/raddb) +RAD_BACKUP="${CONF_BACKUP_DIR}/freeradius_conf_${TIMESTAMP}.tar.gz" +FOUND_RAD=0 +for d in "${RADDIR_CANDIDATES[@]}"; do + if [ -d "$d" ]; then + log "[RUN] backup freeradius conf (${d}) -> ${RAD_BACKUP}" + if [ "$DRY_RUN" -eq 0 ]; then + tar -C / -czf "${RAD_BACKUP}" "${d#/}" || true + fi + log "[OK] freeradius conf backup attempted" + FOUND_RAD=1 + break + fi +done +if [ "$FOUND_RAD" -eq 0 ]; then + log "[WARN] freeradius config directory not found, skipped" +fi + +# Backup php-fpm pool dirs +PHP_FPM_BACKUP="${CONF_BACKUP_DIR}/phpfpm_pools_${TIMESTAMP}.tar.gz" +log "[RUN] backup php-fpm pool confs -> ${PHP_FPM_BACKUP}" +PHP_POOL_DIRS=(/etc/php/*/fpm/pool.d) +exists=() +for p in ${PHP_POOL_DIRS[@]}; do + if compgen -G "$p" >/dev/null; then + exists+=("$p") + fi +done +if [ "${#exists[@]}" -gt 0 ]; then + if [ "$DRY_RUN" -eq 0 ]; then + tar -czf "${PHP_FPM_BACKUP}" "${exists[@]}" 2>/dev/null || true + fi + log "[OK] php-fpm pool backup processed" +else + log "[WARN] no php-fpm pool dirs found, skipped" +fi + +# Backup MariaDB config files +MARIADB_BACKUP="${CONF_BACKUP_DIR}/mariadb_conf_${TIMESTAMP}.tar.gz" +MARIADB_CAND=(/etc/mysql /etc/mysql/mariadb.conf.d /etc/mysql/conf.d /etc/my.cnf /etc/mysql/my.cnf) +log "[RUN] backup mariadb configs -> ${MARIADB_BACKUP}" +to_tar=() +for p in "${MARIADB_CAND[@]}"; do + [ -e "$p" ] && to_tar+=( "$p" ) +done +if [ "${#to_tar[@]}" -gt 0 ]; then + if [ "$DRY_RUN" -eq 0 ]; then + tar -czf "${MARIADB_BACKUP}" "${to_tar[@]}" 2>/dev/null || true + fi + log "[OK] mariadb config backup processed" +else + log "[WARN] no mariadb config files found to backup" +fi + +log "" + +# ------------------------------------------------------- +# Step 3: Enforce backup retention policy (keep last N) +# ------------------------------------------------------- +remove_old() { + local dir="$1" + find "$dir" -maxdepth 1 -type f -printf '%T@ %p\n' | sort -n | awk -v keep="$KEEP" '{files[NR]=$2} END{n=NR; for(i=1;i<=n-keep;i++){ if(i>0 && i<=n){ print files[i] }}}' | while read -r old; do + [ -n "$old" ] || continue + if [ "$DRY_RUN" -eq 0 ]; then rm -f "$old" || true; fi + log "[REMOVED] $old" + done || true +} + +log "========================" +log " Step 3: Backup retention — keep last ${KEEP}" +log "------------------------" +remove_old "${DB_BACKUP_DIR}" +remove_old "${CONF_BACKUP_DIR}" +log "" + +# ---------------------------------------------------------------------- +# Step 4: Schema adjustments — indexes and safe column modifications +# ---------------------------------------------------------------------- +log "========================" +log " Step 4: Ensure required indexes & column sizes on radius/vouchers" +log "------------------------" + +column_exists() { + local table="$1" col="$2" + col=$(echo "$col" | sed -E 's/\(.+$//') + "${MYSQL_BIN}" -u "${MYSQL_USER}" -sN -e "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA='${DB_NAME}' AND TABLE_NAME='${table}' AND COLUMN_NAME='${col}';" 2>/dev/null | grep -q '^1$' +} + +index_exists() { + local table="$1" idx="$2" + "${MYSQL_BIN}" -u "${MYSQL_USER}" -sN -e "SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA='${DB_NAME}' AND TABLE_NAME='${table}' AND INDEX_NAME='${idx}';" 2>/dev/null | grep -q '^1$' +} + +index_on_columns_exists() { + local table="$1" cols="$2" normalized cols_no_spaces existing + normalized=$(echo "$cols" | sed -E 's/\([^)]+\)//g; s/^[[:space:]]+//; s/[[:space:]]+$//; s/[[:space:]]*,[[:space:]]*/,/g') + cols_no_spaces=$(echo "$normalized" | tr -d ' ') + existing=$("${MYSQL_BIN}" -u "${MYSQL_USER}" -sN -e "\ + SELECT INDEX_NAME\ + FROM information_schema.STATISTICS\ + WHERE TABLE_SCHEMA='${DB_NAME}' AND TABLE_NAME='${table}'\ + GROUP BY INDEX_NAME\ + HAVING REPLACE(GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX SEPARATOR ','), ' ', '') = '${cols_no_spaces}'\ + LIMIT 1;" 2>/dev/null || echo "") + [ -n "$existing" ] && echo "$existing" || echo "" +} + +ensure_index() { + local table="$1" idx="$2" cols="$3" sql + IFS=',' read -ra colarr <<< "$cols" + for c in "${colarr[@]}"; do + local rawc + rawc=$(echo "$c" | sed -E 's/^[[:space:]]*//;s/[[:space:]]*$//;s/\(.+$//') + if ! column_exists "$table" "$rawc"; then + log "[WARN] ${table} missing column '${rawc}'; skipping index ${idx} (requires ${cols})" + return 0 + fi + done + + if index_exists "$table" "$idx"; then + log "[OK] index ${idx} exists on ${table}; skipping" + return 0 + fi + + local covering + covering=$(index_on_columns_exists "$table" "$cols") + if [ -n "$covering" ]; then + log "[OK] index '${covering}' already covers columns (${cols}) on ${table}; skipping duplicate" + return 0 + fi + + sql="ALTER TABLE \`${DB_NAME}\`.\`${table}\` ADD INDEX \`${idx}\` (${cols});" + log "[RUN] create index ${idx} on ${table} cols (${cols})" + log "$sql" + if [ "$DRY_RUN" -eq 0 ]; then + set +e + "${MYSQL_DB_CMD[@]}" -e "$sql" 2>&1 + local rc=$? + set -e + if [ $rc -ne 0 ]; then + if [ -n "${PTOSC}" ]; then + log "[INFO] direct ALTER failed; trying pt-online-schema-change for ${table}.${idx}" + set +e + ${PTOSC} --alter "ADD INDEX \`${idx}\` (${cols})" D=${DB_NAME},t=${table} --execute + local pt_rc=$? + set -e + if [ $pt_rc -ne 0 ]; then + log "[ERROR] pt-online-schema-change failed for ${table}.${idx} (exit ${pt_rc})." + else + log "[OK] pt-online-schema-change created ${idx} on ${table}." + fi + else + log "[ERROR] ALTER TABLE failed and Percona Toolkit not available; consider maintenance window for index ${idx} on ${table}." + fi + else + log "[OK] index ${idx} created on ${table}" + fi + else + log "[DRY] would run: $sql" + fi +} + +declare -a IDX_SPECS=( + "radacct|idx_acctuniqueid|acctuniqueid" + "radacct|idx_radacct_user_time|username(50),acctstarttime" + "radacct|idx_radacct_nas_ip|nasipaddress" + "radacct|idx_radacct_framed|framedipaddress" + "radpostauth|idx_authdate|authdate" + "radcheck|idx_username|username" + "radreply|idx_username_attr|username,attribute" + "hotspot_sessions|idx_payment_voucher|payment_id,voucher" + "hotspot_sessions|idx_mac|mac" + "hotspot_sessions|idx_voucher|voucher" + "hotspot_sessions|idx_created_at|created_at" + "vouchers|idx_vouchers_expiration|expiration_time" + "vouchers|idx_vouchers_status_exp|status,expiration_time" + "vouchers|idx_vouchers_plan_status|plan_id,status" + "vouchers|idx_vouchers_location|location_id" + "vouchers|idx_vouchers_phone|phone" + "vouchers|idx_code|code" + "vouchers|idx_created_at|created_at" + "vouchers|idx_status|status" + "vouchers|idx_phone|phone" + "vouchers|idx_expiration_time|expiration_time" +) + +for spec in "${IDX_SPECS[@]}"; do + IFS='|' read -r tbl idx cols <<< "$spec" + ensure_index "$tbl" "$idx" "$cols" +done + +log "[RUN] Analyze tables to refresh optimizer stats" +for t in radacct radpostauth hotspot_sessions vouchers; do + if [ "$DRY_RUN" -eq 0 ]; then + "${MYSQL_DB_CMD[@]}" -e "ANALYZE TABLE \`${DB_NAME}\`.\`${t}\`;" 2>>"${LOG}" || log "[WARN] ANALYZE TABLE ${t} failed or not necessary" + else + log "[DRY] would run: ANALYZE TABLE ${DB_NAME}.${t}" + fi +done +log "[OK] Table analysis requested for radacct, radpostauth, hotspot_sessions, vouchers" + +log "[RUN] ensure radacct.nasportid VARCHAR(255)" +if [ "$DRY_RUN" -eq 0 ]; then + set +e + "${MYSQL_BIN}" -u "${MYSQL_USER}" -sN -e "SELECT CHARACTER_MAXIMUM_LENGTH FROM information_schema.COLUMNS WHERE TABLE_SCHEMA='${DB_NAME}' AND TABLE_NAME='radacct' AND COLUMN_NAME='nasportid';" 2>/dev/null | awk '{print $1}' | grep -q '^255$' + col_is_255=$? + set -e + if [ $col_is_255 -ne 0 ]; then + if ! "${MYSQL_DB_CMD[@]}" -e "ALTER TABLE \`${DB_NAME}\`.radacct MODIFY nasportid VARCHAR(255) DEFAULT NULL;" 2>>"${LOG}"; then + log "[ERROR] Failed to resize nasportid column; it may already be correct or error occurred." + else + log "[OK] Column nasportid resized to VARCHAR(255)" + fi + else + log "[OK] Column nasportid already VARCHAR(255); no change" + fi +else + log "[DRY] would run: ALTER TABLE ${DB_NAME}.radacct MODIFY nasportid VARCHAR(255) DEFAULT NULL;" +fi + +log "" + +# ----------------------------------------------------- +# Step 5: Compute and apply enhanced MariaDB tuning +# ----------------------------------------------------- +log "========================" +log " Step 5: Enhanced MariaDB InnoDB tuning (buffer/log/conn/tmp)" +log "------------------------" + +TOTAL_MEM_KB=$(awk '/MemTotal/ {print $2}' /proc/meminfo 2>/dev/null || echo $((RAM_MB*1024))) +TOTAL_MEM_BYTES=$((TOTAL_MEM_KB * 1024)) +MIN_POOL_BYTES=$((1 * 1024 * 1024 * 1024)) +CALC_POOL_BYTES=$(awk -v m="$TOTAL_MEM_BYTES" 'BEGIN{printf("%d", m*0.50)}') +INNODB_BUFFER_POOL_SIZE_BYTES=$(( CALC_POOL_BYTES < MIN_POOL_BYTES ? MIN_POOL_BYTES : CALC_POOL_BYTES )) +ROUND_128MB=$((128 * 1024 * 1024)) +INNODB_BUFFER_POOL_SIZE_BYTES=$(( (INNODB_BUFFER_POOL_SIZE_BYTES / ROUND_128MB) * ROUND_128MB )) +if [ "$INNODB_BUFFER_POOL_SIZE_BYTES" -lt "$MIN_POOL_BYTES" ]; then INNODB_BUFFER_POOL_SIZE_BYTES=$MIN_POOL_BYTES; fi +INNODB_BUFFER_POOL_SIZE_MB=$(( INNODB_BUFFER_POOL_SIZE_BYTES / 1024 / 1024 )) + +INNODB_BUFFER_POOL_INSTANCES=$(( INNODB_BUFFER_POOL_SIZE_MB / 1024 )) +if [ "$INNODB_BUFFER_POOL_INSTANCES" -lt 1 ]; then INNODB_BUFFER_POOL_INSTANCES=1; fi +if [ "$INNODB_BUFFER_POOL_INSTANCES" -gt 8 ]; then INNODB_BUFFER_POOL_INSTANCES=8; fi + +LOG_FILE_SIZE_BYTES=$(( INNODB_BUFFER_POOL_SIZE_BYTES / 4 )) +MAX_LOG_BYTES=$((1 * 1024 * 1024 * 1024)) +if [ "$LOG_FILE_SIZE_BYTES" -gt "$MAX_LOG_BYTES" ]; then LOG_FILE_SIZE_BYTES=$MAX_LOG_BYTES; fi +ROUND_16MB=$((16 * 1024 * 1024)) +INNODB_LOG_FILE_SIZE_BYTES=$(( (LOG_FILE_SIZE_BYTES / ROUND_16MB) * ROUND_16MB )) +INNODB_LOG_FILE_SIZE_MB=$(( INNODB_LOG_FILE_SIZE_BYTES / 1024 / 1024 )) + +MAX_CONNECTIONS=$(( VCPUS * 150 )) +if [ "$MAX_CONNECTIONS" -gt 2000 ]; then MAX_CONNECTIONS=2000; fi + +TMP_TABLE_SIZE_MB=$(( INNODB_BUFFER_POOL_SIZE_MB / 8 )) +if [ "$TMP_TABLE_SIZE_MB" -lt 64 ]; then TMP_TABLE_SIZE_MB=64; fi +if [ "$TMP_TABLE_SIZE_MB" -gt 1024 ]; then TMP_TABLE_SIZE_MB=1024; fi + +INNODB_IO_THREADS=$(( VCPUS < 4 ? 4 : VCPUS )) +INNODB_READ_IO_THREADS=$INNODB_IO_THREADS +INNODB_WRITE_IO_THREADS=$INNODB_IO_THREADS + +log "[INFO] Buffer pool size: ${INNODB_BUFFER_POOL_SIZE_MB}M" +log "[INFO] Buffer pool instances: ${INNODB_BUFFER_POOL_INSTANCES}" +log "[INFO] Log file size: ${INNODB_LOG_FILE_SIZE_MB}M" +log "[INFO] Max connections: ${MAX_CONNECTIONS}" +log "[INFO] tmp_table_size/max_heap_table_size: ${TMP_TABLE_SIZE_MB}M" +log "[INFO] InnoDB IO threads (read/write): ${INNODB_READ_IO_THREADS}/${INNODB_WRITE_IO_THREADS}" + +if [ -f "${MARIADB_FRAG}" ]; then + log "[RUN] backup existing MariaDB fragment -> ${MARIADB_FRAG}.bak.${TIMESTAMP}" + [ "$DRY_RUN" -eq 0 ] && cp -a "${MARIADB_FRAG}" "${MARIADB_FRAG}.bak.${TIMESTAMP}" +fi + +MARIADB_FRAG_TMP="${MARIADB_FRAG}.tmp" +cat > "${MARIADB_FRAG_TMP}" < ${MARIADB_FRAG}" + rm -f "${MARIADB_FRAG_TMP}" || true +fi + +log "" + +# ------------------------------------------------------------------ +# Step 6: FreeRADIUS thread pool tuning in radiusd.conf (backup first) +# ------------------------------------------------------------------ +log "========================" +log " Step 6: Update radiusd.conf thread pool values" +log "------------------------" + +RADIUS_PATHS=(/etc/freeradius/radiusd.conf /etc/freeradius/3.0/radiusd.conf /etc/raddb/radiusd.conf /etc/freeradius/3.2/radiusd.conf) +RADIUS_CONF="" +for p in "${RADIUS_PATHS[@]}"; do + if [ -f "$p" ]; then + RADIUS_CONF="$p" + break + fi +done + +if [ -z "${RADIUS_CONF}" ]; then + log "[WARN] radiusd.conf not found in common paths; skipping thread-pool edit." +else + START_SERVERS=$(( VCPUS / 2 )); [ "$START_SERVERS" -lt 2 ] && START_SERVERS=2 + MAX_SERVERS=$(( VCPUS * 6 )) + MIN_SPARE=$(( VCPUS / 2 )); [ "$MIN_SPARE" -lt 2 ] && MIN_SPARE=2 + MAX_SPARE=$(( VCPUS * 2 )); [ "$MAX_SPARE" -lt "$MIN_SPARE" ] && MAX_SPARE=$MIN_SPARE + + log "[RUN] Using radiusd.conf: ${RADIUS_CONF}" + [ "$DRY_RUN" -eq 0 ] && cp -a "${RADIUS_CONF}" "${RADIUS_CONF}.bak.${TIMESTAMP}" + TMP_RAD="${RADIUS_CONF}.new" + awk -v s="${START_SERVERS}" -v m="${MAX_SERVERS}" -v minsp="${MIN_SPARE}" -v maxsp="${MAX_SPARE}" ' + BEGIN{inblock=0} + { + if($0 ~ /thread pool[[:space:]]*{/) { print; inblock=1; next } + if(inblock && $0 ~ /^[[:space:]]*start_servers[[:space:]]*=/) { printf " start_servers = %s\n", s; next } + if(inblock && $0 ~ /^[[:space:]]*max_servers[[:space:]]*=/) { printf " max_servers = %s\n", m; next } + if(inblock && $0 ~ /^[[:space:]]*min_spare_servers[[:space:]]*=/) { printf " min_spare_servers = %s\n", minsp; next } + if(inblock && $0 ~ /^[[:space:]]*max_spare_servers[[:space:]]*=/) { printf " max_spare_servers = %s\n", maxsp; next } + if(inblock && $0 ~ /^}/) { inblock=0; print; next } + print + } + ' "${RADIUS_CONF}" > "${TMP_RAD}" || true + + if [ "$DRY_RUN" -eq 0 ]; then + mv -f "${TMP_RAD}" "${RADIUS_CONF}" + log "[OK] radiusd.conf thread pool updated (backup: ${RADIUS_CONF}.bak.${TIMESTAMP})" + else + rm -f "${TMP_RAD}" || true + log "[DRY] would update radiusd.conf thread pool" + fi +fi + +log "" + +# --------------------------------------------------------------------- +# Step 7: PHP-FPM detection, tuning, slowlog setup, and IN-PLACE edits +# --------------------------------------------------------------------- +log "========================" +log " Step 7: PHP-FPM replacement (REPLACE existing directives in-place)" +log "------------------------" + +PHP_VER_DETECTED="${PHP_VER_OVERRIDE:-}" +DETECTION_METHOD="" + +detect_active_php_via_systemctl() { + local svc_list svc_name ver + svc_list=$(systemctl list-units --type=service --no-pager --no-legend | awk '{print $1}' | grep -E '^php[0-9]+(\.[0-9]+)?-fpm\.service$' || true) + for svc_name in $svc_list; do + # strip .service suffix + local svc_clean="${svc_name%.service}" + if systemctl is-active --quiet "$svc_clean"; then + ver=$(echo "$svc_clean" | sed -E 's/^php([0-9]+(\.[0-9]+)?)-fpm$/\1/') + echo "$ver" + return 0 + fi + done + return 1 +} + +detect_php_via_cli_if_pool_exists() { + if ! command -v php >/dev/null 2>&1; then + return 1 + fi + local cli_ver pool major pool_alt + cli_ver=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;' 2>/dev/null || true) + [ -z "$cli_ver" ] && return 1 + pool="/etc/php/${cli_ver}/fpm/pool.d/www.conf" + if [ -f "$pool" ]; then + echo "$cli_ver"; return 0 + fi + major=$(echo "$cli_ver" | cut -d. -f1) + pool_alt="/etc/php/${major}/fpm/pool.d/www.conf" + if [ -f "$pool_alt" ]; then + echo "$cli_ver"; return 0 + fi + return 1 +} + +if [ -z "${PHP_VER_DETECTED}" ]; then + if ver=$(detect_active_php_via_systemctl); then + PHP_VER_DETECTED="$ver"; DETECTION_METHOD="systemctl-active" + fi +fi +if [ -z "${PHP_VER_DETECTED}" ]; then + if ver=$(detect_php_via_cli_if_pool_exists); then + PHP_VER_DETECTED="$ver"; DETECTION_METHOD="cli-fallback-with-pool-check" + fi +fi + +if [ -z "${PHP_VER_DETECTED}" ]; then + log "[WARN] Could not detect PHP-FPM version; set PHP_VER_OVERRIDE or start phpX.Y-fpm. Skipping PHP-FPM edit." +else + POOL_CONF="/etc/php/${PHP_VER_DETECTED}/fpm/pool.d/www.conf" + PHPFPM_SERVICE="php${PHP_VER_DETECTED}-fpm" + SLOWLOG_PATH="/var/log/php${PHP_VER_DETECTED}-fpm/www-slow.log" + if [ ! -f "$POOL_CONF" ]; then + major=$(echo "$PHP_VER_DETECTED" | cut -d. -f1) + alt_pool="/etc/php/${major}/fpm/pool.d/www.conf" + alt_service="php${major}-fpm" + alt_slowlog="/var/log/php${major}-fpm/www-slow.log" + if [ -f "$alt_pool" ]; then + POOL_CONF="$alt_pool"; PHPFPM_SERVICE="$alt_service"; SLOWLOG_PATH="$alt_slowlog" + fi + fi + + log "[INFO] Detected PHP version: ${PHP_VER_DETECTED} (method: ${DETECTION_METHOD:-unknown})" + log "[INFO] Using POOL_CONF: ${POOL_CONF}" + log "[INFO] Using PHPFPM_SERVICE: ${PHPFPM_SERVICE}" + log "[INFO] Using SLOWLOG_PATH: ${SLOWLOG_PATH}" + + # Ensure slowlog file exists and has safe perms + SLOWLOG_DIR="$(dirname "$SLOWLOG_PATH")" + if [ "$DRY_RUN" -eq 0 ]; then + [ -d "$SLOWLOG_DIR" ] || mkdir -p "$SLOWLOG_DIR" + [ -f "$SLOWLOG_PATH" ] || touch "$SLOWLOG_PATH" + if id -u "$FPM_USER" >/dev/null 2>&1; then + chown -R "$FPM_USER":"$FPM_GROUP" "$SLOWLOG_DIR" || true + fi + chmod 750 "$SLOWLOG_DIR" || true + chmod 640 "$SLOWLOG_PATH" || true + else + log "[DRY] would ensure slowlog dir/file and set perms" + fi + + # Backup existing pool file + PHPFPM_POOL_BAK="${CONF_BACKUP_DIR}/www.conf.bak.${TIMESTAMP}" + if [ -f "$POOL_CONF" ]; then + if [ "$DRY_RUN" -eq 0 ]; then cp -a "$POOL_CONF" "$PHPFPM_POOL_BAK"; fi + log "[OK] Backed up pool config -> ${PHPFPM_POOL_BAK}" + else + log "[WARN] PHP-FPM pool file not found at ${POOL_CONF}; skipping pm.* replacement" + fi + + # ------------------------- + # Remove existing AUTOTUNE block (if present) + # ------------------------- + if [ -f "$POOL_CONF" ]; then + if [ "$DRY_RUN" -eq 0 ]; then + if command -v perl >/dev/null 2>&1; then + # Remove inclusive block between lines containing AUTOTUNE BEGIN/END (handles leading ; or #) + perl -0777 -pe 's/^[\t ]*[;#]*[\t ]*=== AUTOTUNE BEGIN ===.*?^[\t ]*[;#]*[\t ]*=== AUTOTUNE END ===\s*\n?//gms' -i "$POOL_CONF" \ + && log "[OK] removed existing AUTOTUNE block from ${POOL_CONF} (if present)" + else + # Fallback using awk/sed: remove lines between markers (less flexible but works for typical cases) + awk 'BEGIN{skip=0} + { + if($0 ~ /^[[:space:]]*[;#]*[[:space:]]*=== AUTOTUNE BEGIN ===/) { skip=1; next } + if($0 ~ /^[[:space:]]*[;#]*[[:space:]]*=== AUTOTUNE END ===/) { skip=0; next } + if(!skip) print + }' "$POOL_CONF" > "${POOL_CONF}.noautotune" && mv -f "${POOL_CONF}.noautotune" "$POOL_CONF" \ + && log "[OK] removed AUTOTUNE block (fallback) from ${POOL_CONF} (if present)" + fi + else + log "[DRY] would remove existing AUTOTUNE block from ${POOL_CONF} (if present)" + fi + fi + + # If pool file exists, compute values and do in-place replacements + if [ -f "$POOL_CONF" ]; then + # Compute recommended values + CPU_CORES=$(nproc --all 2>/dev/null || echo 1) + TOTAL_RAM_MB=$(free -m | awk '/^Mem:/{print $2}') + RESERVE_BY_PERCENT=$(awk -v r="$TOTAL_RAM_MB" 'BEGIN{printf "%.0f", r*0.15}') + RESERVE_MB=$(( RESERVE_BY_PERCENT + CPU_CORES*200 )) + [ "$RESERVE_MB" -lt 1024 ] && RESERVE_MB=1024 + AVAILABLE_FOR_PHP_MB=$(( TOTAL_RAM_MB - RESERVE_MB )) + [ "$AVAILABLE_FOR_PHP_MB" -lt 256 ] && AVAILABLE_FOR_PHP_MB=256 + + # Determine avg worker RSS (best-effort) + PHPFPM_PROCNAMES=("php-fpm${PHP_VER_DETECTED}" "php${PHP_VER_DETECTED}-fpm" "php-fpm" "php7.4-fpm" "php8.1-fpm" "php8.2-fpm") + FOUND_PROCNAME="" + for pn in "${PHPFPM_PROCNAMES[@]}"; do + if pgrep -x "$pn" >/dev/null 2>&1; then FOUND_PROCNAME="$pn"; break; fi + done + AVG_RSS_KB=0 + if [ -n "$FOUND_PROCNAME" ]; then + AVG_RSS_KB=$(ps --no-headers -o rss -C "$FOUND_PROCNAME" 2>/dev/null | awk '{s+=$1;n++}END{if(n)printf "%.0f",s/n;else print 0}') + fi + if [ -z "${AVG_RSS_KB}" ] || [ "$AVG_RSS_KB" -le 0 ]; then + AVG_RSS_MB=50 + AVG_RSS_KB=$((AVG_RSS_MB * 1024)) + else + AVG_RSS_MB=$(( (AVG_RSS_KB + 1023) / 1024 )) + fi + + MAX_CHILDREN=$(( AVAILABLE_FOR_PHP_MB / AVG_RSS_MB )) + [ "$MAX_CHILDREN" -lt 2 ] && MAX_CHILDREN=2 + [ "$MAX_CHILDREN" -gt 1000 ] && MAX_CHILDREN=1000 + PM_START=$(( MAX_CHILDREN * 20 / 100 )) + PM_MIN=$(( MAX_CHILDREN * 10 / 100 )) + PM_MAX=$(( MAX_CHILDREN * 30 / 100 )) + [ "$PM_START" -lt 2 ] && PM_START=2 + [ "$PM_MIN" -lt 1 ] && PM_MIN=1 + [ "$PM_MAX" -lt 2 ] && PM_MAX=2 + + # ------------------------- + # In-place replacement API + # ------------------------- + ensure_directive() { + local file="$1" key="$2" val="$3" anchor="$4" + local val_esc + val_esc=$(printf '%s' "$val" | sed -e 's|[\/&]|\\&|g') + # Replace any existing line (commented or not) + if grep -qE "^[[:space:]]*[;#]?[[:space:]]*${key}[[:space:]]*=" "$file"; then + if [ "$DRY_RUN" -eq 0 ]; then + sed -i -E "s|^[[:space:]]*[;#]?[[:space:]]*(${key})[[:space:]]*=.*|\1 = ${val_esc}|" "$file" + log "[OK] replaced ${key} in ${file}" + else + log "[DRY] would replace ${key} = ${val} in ${file}" + fi + else + # No existing line — insert after anchor (anchor is anchor key, e.g., 'pm' for 'pm = dynamic') + if [ -n "$anchor" ] && grep -qE "^[[:space:]]*${anchor}[[:space:]]*=" "$file"; then + if [ "$DRY_RUN" -eq 0 ]; then + awk -v a="$anchor" -v newline="${key} = ${val}" '{ + print; + if(!inserted && $0 ~ "^[[:space:]]*"a"[[:space:]]*="){ print newline; inserted=1 } + } END{ if(!inserted) print newline }' "$file" > "${file}.tmp" && mv -f "${file}.tmp" "$file" + log "[OK] inserted ${key} after ${anchor} in ${file}" + else + log "[DRY] would insert ${key} = ${val} after ${anchor} in ${file}" + fi + else + if [ "$DRY_RUN" -eq 0 ]; then + echo "${key} = ${val}" >> "$file" + log "[OK] appended ${key} to ${file}" + else + log "[DRY] would append ${key} = ${val} to ${file}" + fi + fi + fi + } + + # Remove commented duplicates of pm.* and slowlog/status lines (clean up leftovers) + if [ "$DRY_RUN" -eq 0 ]; then + sed -i -E '/^[[:space:]]*[;#][[:space:]]*(pm\.max_children|pm\.start_servers|pm\.min_spare_servers|pm\.max_spare_servers|pm\.max_requests|request_slowlog_timeout|slowlog|pm\.status_path|catch_workers_output)[[:space:]]*=.*$/d' "$POOL_CONF" || true + else + log "[DRY] would remove commented duplicates of pm.* and slowlog lines in ${POOL_CONF}" + fi + + # Ensure directives in-place, using anchor "pm" to insert near pm = dynamic if needed + ensure_directive "$POOL_CONF" "pm" "dynamic" "" + ensure_directive "$POOL_CONF" "pm.max_children" "${MAX_CHILDREN}" "pm" + ensure_directive "$POOL_CONF" "pm.start_servers" "${PM_START}" "pm" + ensure_directive "$POOL_CONF" "pm.min_spare_servers" "${PM_MIN}" "pm" + ensure_directive "$POOL_CONF" "pm.max_spare_servers" "${PM_MAX}" "pm" + ensure_directive "$POOL_CONF" "pm.max_requests" "${PM_MAX_REQUESTS}" "pm" + ensure_directive "$POOL_CONF" "request_slowlog_timeout" "${REQUEST_SLOWLOG_TIMEOUT}" "pm" + ensure_directive "$POOL_CONF" "slowlog" "${SLOWLOG_PATH}" "pm" + ensure_directive "$POOL_CONF" "pm.status_path" "${STATUS_PATH}" "pm" + ensure_directive "$POOL_CONF" "catch_workers_output" "${CATCH_WORKERS_OUTPUT}" "pm" + + if [ "$DRY_RUN" -eq 0 ]; then + chmod 640 "$POOL_CONF" || true + log "[OK] PHP-FPM pool file updated in-place (max_children=${MAX_CHILDREN})" + else + log "[DRY] would update pool file in-place (max_children=${MAX_CHILDREN})" + fi + + # Reload/restart php-fpm + if [ "$DRY_RUN" -eq 0 ]; then + if systemctl reload "$PHPFPM_SERVICE" >/dev/null 2>&1; then + log "[OK] Reloaded $PHPFPM_SERVICE successfully." + else + log "[WARN] Reload failed; attempting restart..." + if systemctl restart "$PHPFPM_SERVICE" >/dev/null 2>&1; then + log "[OK] Restarted $PHPFPM_SERVICE successfully." + else + log "[ERROR] Restart failed, please check $PHPFPM_SERVICE and logs manually." + fi + fi + else + log "[DRY] would reload/restart $PHPFPM_SERVICE" + fi + + # Write suggestion report + cat > "$PHPFPM_SUGGESTION_FILE" </dev/null | awk '{print $1}' | grep -qi "^${svc}.service$"; then + present=1 + fi + + log "[RUN] restart ${svc} (present=${present})" + + if [ "$DRY_RUN" -eq 1 ]; then + log "[DRY] would restart ${svc}" + return 0 + fi + + if systemctl restart "${svc}" >/dev/null 2>&1; then + restarted=1 + else + if command -v service >/dev/null 2>&1; then + if service "${svc}" restart >/dev/null 2>&1; then + restarted=1 + fi + fi + fi + + if [ "$restarted" -eq 1 ]; then + if systemctl is-active --quiet "${svc}" 2>/dev/null; then + log "[OK] ${svc} is active" + else + if command -v service >/dev/null 2>&1 && service "${svc}" status >/dev/null 2>&1; then + log "[OK] ${svc} status reports running via SysV" + else + log "[WARN] ${svc} restarted but status not confirmed; check: journalctl -u ${svc} -n 200 --no-pager" + fi + fi + return 0 + else + log "[ERROR] failed to restart ${svc} via systemctl and service; verify service name and packaging" + return 1 + fi +} + +MARIADB_RESTARTED=0 +for candidate in mariadb mysql mysqld; do + if restart_and_check "$candidate"; then + MARIADB_RESTARTED=1 + break + fi +done +if [ "$MARIADB_RESTARTED" -eq 0 ]; then + log "[WARN] MariaDB/MySQL restart not confirmed; ensure correct service name (mariadb/mysql/mysqld)" +fi + +RAD_RESTARTED=0 +for candidate in freeradius radiusd; do + if restart_and_check "$candidate"; then + RAD_RESTARTED=1 + break + fi +done +if [ "$RAD_RESTARTED" -eq 0 ]; then + log "[WARN] FreeRADIUS restart not confirmed; check OS packaging (freeradius or radiusd)" +fi + +if [ -n "${PHPFPM_SERVICE:-}" ]; then + restart_and_check "$PHPFPM_SERVICE" +fi + +PHP_FPM_UNITS=$(systemctl list-units --type=service --all --no-legend 2>/dev/null | awk '{print $1}' | grep -E '^php[0-9]+\.[0-9]+-fpm\.service$' || true) +if [ -n "$PHP_FPM_UNITS" ]; then + for u in $PHP_FPM_UNITS; do + svcname="${u%.service}" + restart_and_check "$svcname" + done +else + for ver in 8.2 8.1 8.0 7.4 7.3; do + restart_and_check "php${ver}-fpm" + done +fi + +log "" + +# ---------------------------------- +# Step 9: Summary and logs +# ---------------------------------- +log "========================" +log " Summary" +log "------------------------" +log "1) MariaDB config fragment: ${MARIADB_FRAG}" +log "2) radiusd.conf updated at: ${RADIUS_CONF:-}" +log "3) php-fpm pool edited in-place: ${POOL_CONF:-}" +log "4) Backups directory: ${BACKUP_BASE}" +log "5) Run log: ${LOG}" +log "" +log "Completed universal_replaced_complete run. Review the log and service journals if any step reported warnings or errors." +# release lock implicitly on script exit +exit 0 From 9df9aa636eef9c3d944e485cb90ac148f44b2ca4 Mon Sep 17 00:00:00 2001 From: Barnabie Opiyo <1921395+mymanga@users.noreply.github.com> Date: Wed, 7 Jan 2026 02:10:37 +0300 Subject: [PATCH 13/45] Delete mmsqlconf.sh --- mmsqlconf.sh | 429 --------------------------------------------------- 1 file changed, 429 deletions(-) delete mode 100644 mmsqlconf.sh diff --git a/mmsqlconf.sh b/mmsqlconf.sh deleted file mode 100644 index 7f24d8b..0000000 --- a/mmsqlconf.sh +++ /dev/null @@ -1,429 +0,0 @@ -#!/bin/bash - -# Error handling function -error_exit() { - echo "Error: $1" >&2 - exit 1 -} - -echo "Starting MariaDB configuration optimization for FreeRADIUS..." - -# Check if running as root -if [ "$(id -u)" -ne 0 ]; then - error_exit "This script must be run as root. Try using sudo." -fi - -# Check if MariaDB is installed -if ! command -v mariadb &> /dev/null; then - error_exit "MariaDB is not installed. Please install it first." -fi - -# Function to aggressively kill all MariaDB processes -kill_all_mariadb() { - echo "Aggressively terminating all MariaDB processes..." - - # Stop the service first - systemctl stop mariadb - sleep 2 - - # Find and kill all MariaDB processes - for PROC in mysqld mariadbd mysql mariadb; do - # Try with pkill first (process name) - pkill -9 $PROC 2>/dev/null || true - - # Try with pkill -f (command line) - pkill -9 -f $PROC 2>/dev/null || true - done - - # Find any remaining MySQL processes by port - for PID in $(lsof -i:3306 -t 2>/dev/null); do - kill -9 $PID 2>/dev/null || true - done - - # Wait to ensure processes are terminated - sleep 3 - - # Check if any processes are still running - if pgrep -x "mysqld" > /dev/null || pgrep -x "mariadbd" > /dev/null || pgrep -f "mysql" > /dev/null; then - echo "Warning: Some MariaDB processes are still running. This may cause issues." - else - echo "All MariaDB processes have been terminated." - fi -} - -# Function to clean up lock files -cleanup_lock_files() { - echo "Cleaning up lock files..." - - # Remove aria log files - rm -f /var/lib/mysql/aria_log_control 2>/dev/null || true - rm -f /var/lib/mysql/aria_log.* 2>/dev/null || true - - # Remove InnoDB lock files - rm -f /var/lib/mysql/ib_logfile* 2>/dev/null || true - rm -f /var/lib/mysql/ibdata1.lock 2>/dev/null || true - - # Remove socket files - rm -f /var/run/mysqld/mysqld.sock 2>/dev/null || true - rm -f /tmp/mysql.sock 2>/dev/null || true - - # Remove pid file - rm -f /var/run/mysqld/mysqld.pid 2>/dev/null || true - - # Ensure the mysqld directory exists with proper permissions - mkdir -p /var/run/mysqld 2>/dev/null || true - chown mysql:mysql /var/run/mysqld 2>/dev/null || true - - # Fix permissions on data directory - chown -R mysql:mysql /var/lib/mysql/ 2>/dev/null || true - - echo "Lock files cleanup completed." -} - -# Calculate system resources -TOTAL_RAM=$(free -b | awk '/Mem:/ {print $2}') -CPU_CORES=$(nproc) - -# Calculate RAM allocation (70% for buffer pool) -BUFFER_POOL_SIZE=$((TOTAL_RAM * 70 / 100)) -BUFFER_POOL_SIZE_MB=$((BUFFER_POOL_SIZE / 1024 / 1024)) - -# Calculate CPU-based settings targeting 60% CPU usage -# Each InnoDB instance can use about 15% CPU, so we calculate based on that -INNODB_INSTANCES=$((CPU_CORES * 60 / 100)) # 60% of cores -INNODB_INSTANCES=$((INNODB_INSTANCES > 0 ? INNODB_INSTANCES : 1)) # Minimum 1 - -# IO threads should be proportional to InnoDB instances -IO_THREADS=$((INNODB_INSTANCES * 2)) # 2 threads per instance - -# Thread pool size based on CPU cores (targeting 60% utilization) -THREAD_POOL_SIZE=$((CPU_CORES * 60 / 100)) -THREAD_POOL_SIZE=$((THREAD_POOL_SIZE > 0 ? THREAD_POOL_SIZE : 1)) - -# Calculate other resource-based settings -MAX_CONNECTIONS=$((CPU_CORES * 100)) # 100 connections per core -MAX_USER_CONNECTIONS=$((MAX_CONNECTIONS * 80 / 100)) # 80% of max connections - -# Calculate buffer sizes based on available RAM -PER_THREAD_BUFFERS=$((TOTAL_RAM * 5 / 100 / MAX_CONNECTIONS)) # 5% of RAM divided by max connections -SORT_BUFFER_SIZE=$((PER_THREAD_BUFFERS / 4)) -READ_BUFFER_SIZE=$((PER_THREAD_BUFFERS / 4)) -JOIN_BUFFER_SIZE=$((PER_THREAD_BUFFERS / 4)) -READ_RND_BUFFER_SIZE=$((PER_THREAD_BUFFERS / 4)) - -# Convert buffer sizes to MB with minimum values -SORT_BUFFER_SIZE_MB=$((SORT_BUFFER_SIZE / 1024 / 1024)) -SORT_BUFFER_SIZE_MB=$((SORT_BUFFER_SIZE_MB > 0 ? SORT_BUFFER_SIZE_MB : 1)) - -READ_BUFFER_SIZE_MB=$((READ_BUFFER_SIZE / 1024 / 1024)) -READ_BUFFER_SIZE_MB=$((READ_BUFFER_SIZE_MB > 0 ? READ_BUFFER_SIZE_MB : 1)) - -JOIN_BUFFER_SIZE_MB=$((JOIN_BUFFER_SIZE / 1024 / 1024)) -JOIN_BUFFER_SIZE_MB=$((JOIN_BUFFER_SIZE_MB > 0 ? JOIN_BUFFER_SIZE_MB : 1)) - -READ_RND_BUFFER_SIZE_MB=$((READ_RND_BUFFER_SIZE / 1024 / 1024)) -READ_RND_BUFFER_SIZE_MB=$((READ_RND_BUFFER_SIZE_MB > 0 ? READ_RND_BUFFER_SIZE_MB : 1)) - -# Calculate table cache based on RAM -TABLE_OPEN_CACHE=$((TOTAL_RAM / 1024 / 1024 / 2)) # Roughly 1 cache entry per 2MB RAM - -# Define the MariaDB configuration file locations for Ubuntu 20.04 -MYSQL_CONF_FILES=( - "/etc/mysql/mariadb.conf.d/50-server.cnf" -) - -# Kill all MariaDB processes before making configuration changes -kill_all_mariadb -cleanup_lock_files - -# Loop through the possible configuration file locations -for MYSQL_CONF_FILE in "${MYSQL_CONF_FILES[@]}"; do - # Check if the file exists - if [ -f "$MYSQL_CONF_FILE" ]; then - # Backup the configuration file - BACKUP_FILE="${MYSQL_CONF_FILE}.bak.$(date +%Y%m%d%H%M%S)" - cp "$MYSQL_CONF_FILE" "$BACKUP_FILE" - echo "Backed up $MYSQL_CONF_FILE to $BACKUP_FILE" - - # Create a temporary file for the new configuration - TMP_CONF=$(mktemp) - - # Read the original file to preserve structure and comments - # Extract the [mysqld] section - MYSQLD_SECTION_FOUND=false - CURRENT_SECTION="" - - while IFS= read -r line; do - # Detect section headers - if [[ "$line" =~ ^\[(.*)\] ]]; then - CURRENT_SECTION="${BASH_REMATCH[1]}" - echo "$line" >> "$TMP_CONF" - - # If we found the mysqld section, add our optimized settings - if [[ "$CURRENT_SECTION" == "mysqld" ]]; then - MYSQLD_SECTION_FOUND=true - - # Add our optimized settings for FreeRADIUS - cat >> "$TMP_CONF" << EOF - -# Basic Settings -user = mysql -pid-file = /run/mysqld/mysqld.pid -socket = /run/mysqld/mysqld.sock -port = 3306 -basedir = /usr -datadir = /var/lib/mysql -tmpdir = /tmp -lc-messages-dir = /usr/share/mysql -bind-address = 0.0.0.0 - -# Logging Configuration -log_error = /var/log/mysql/error.log -log_warnings = 2 - -# FreeRADIUS-Optimized InnoDB Settings -innodb_buffer_pool_size = ${BUFFER_POOL_SIZE_MB}M -innodb_buffer_pool_instances = ${INNODB_INSTANCES} -innodb_log_file_size = 512M -innodb_log_buffer_size = 64M -innodb_file_per_table = ON -innodb_open_files = ${TABLE_OPEN_CACHE} -innodb_io_capacity = $((IO_THREADS * 100)) -innodb_flush_method = O_DIRECT -innodb_read_io_threads = ${IO_THREADS} -innodb_write_io_threads = ${IO_THREADS} -innodb_stats_on_metadata = OFF -innodb_flush_log_at_trx_commit = 2 -innodb_doublewrite = 0 -innodb_lock_wait_timeout = 5 -innodb_deadlock_detect = ON - -# Connection Settings for FreeRADIUS Accounting -max_connections = ${MAX_CONNECTIONS} -max_user_connections = ${MAX_USER_CONNECTIONS} -thread_cache_size = $((THREAD_POOL_SIZE * 2)) -thread_stack = 192K -interactive_timeout = 30 -wait_timeout = 30 -max_allowed_packet = 16M -net_read_timeout = 5 -net_write_timeout = 5 -connect_timeout = 5 - -# Thread Pool Settings for Fast Accounting -thread_handling = pool-of-threads -thread_pool_size = ${THREAD_POOL_SIZE} -thread_pool_idle_timeout = 30 -thread_pool_max_threads = $((MAX_CONNECTIONS / 2)) -thread_pool_oversubscribe = 3 - -# Query Cache Settings (disabled for high-concurrency workloads) -query_cache_type = 0 -query_cache_size = 0 - -# Buffer Settings -sort_buffer_size = ${SORT_BUFFER_SIZE_MB}M -read_buffer_size = ${READ_BUFFER_SIZE_MB}M -read_rnd_buffer_size = ${READ_RND_BUFFER_SIZE_MB}M -join_buffer_size = ${JOIN_BUFFER_SIZE_MB}M -tmp_table_size = $((BUFFER_POOL_SIZE_MB / 32))M -max_heap_table_size = $((BUFFER_POOL_SIZE_MB / 32))M - -# Table Settings -table_open_cache = ${TABLE_OPEN_CACHE} -table_definition_cache = $((TABLE_OPEN_CACHE / 2)) -open_files_limit = $((TABLE_OPEN_CACHE * 2)) - -# MyISAM Settings (minimal since we use InnoDB) -key_buffer_size = $((BUFFER_POOL_SIZE_MB / 32))M -myisam_sort_buffer_size = $((BUFFER_POOL_SIZE_MB / 64))M - -# Aria Settings -aria_pagecache_buffer_size = $((BUFFER_POOL_SIZE_MB / 32))M -aria_sort_buffer_size = $((BUFFER_POOL_SIZE_MB / 64))M -aria_group_commit = none -aria_group_commit_interval = 0 -aria_log_purge_type = immediate - -# Security -local-infile = 0 -skip-name-resolve = ON - -# Performance Schema (enable for monitoring) -performance_schema = ON -performance_schema_max_table_instances = $((TABLE_OPEN_CACHE / 2)) - -# Slow Query Logging -slow_query_log = 1 -slow_query_log_file = /var/log/mysql/mariadb-slow.log -long_query_time = 2 - -# MySQL optimization settings to handle high load and connection issues -innodb_lock_wait_timeout = 30 -max_connections = 1000 -innodb_buffer_pool_size = 4G -innodb_log_buffer_size = 64M -innodb_file_per_table = 1 -innodb_flush_log_at_trx_commit = 2 -innodb_flush_method = O_DIRECT -innodb_thread_concurrency = 0 -thread_cache_size = 100 -table_open_cache = 8000 -query_cache_type = 0 -query_cache_size = 0 -max_connect_errors = 999999 -wait_timeout = 600 -interactive_timeout = 600 -skip-name-resolve -innodb_io_capacity = 2000 -innodb_io_capacity_max = 4000 -innodb_read_io_threads = 8 -innodb_write_io_threads = 8 -innodb_buffer_pool_instances = 8 - -EOF - fi - elif [[ "$CURRENT_SECTION" == "mysqld" && "$MYSQLD_SECTION_FOUND" == true ]]; then - # Skip existing settings in the mysqld section as we've already added our optimized ones - continue - else - # Copy the line as is for other sections - echo "$line" >> "$TMP_CONF" - fi - done < "$MYSQL_CONF_FILE" - - # If we didn't find a mysqld section, add one with the same settings - if [[ "$MYSQLD_SECTION_FOUND" == false ]]; then - echo -e "\n[mysqld]" >> "$TMP_CONF" - # Add the same configuration block as above - cat >> "$TMP_CONF" << EOF -# Same configuration block as above... -EOF - fi - - # Check the size of the generated file - TMP_SIZE=$(stat -c %s "$TMP_CONF" 2>/dev/null || stat -f %z "$TMP_CONF") - ORIG_SIZE=$(stat -c %s "$MYSQL_CONF_FILE" 2>/dev/null || stat -f %z "$MYSQL_CONF_FILE") - - if [ "$TMP_SIZE" -gt $((ORIG_SIZE * 10)) ]; then - echo "Warning: Generated configuration file is much larger than original (${TMP_SIZE} vs ${ORIG_SIZE} bytes)." - echo "This may indicate a problem. Aborting to prevent file corruption." - rm -f "$TMP_CONF" - error_exit "Configuration generation failed due to unexpected file size." - fi - - # Replace the original file with the new configuration - mv "$TMP_CONF" "$MYSQL_CONF_FILE" - chmod 644 "$MYSQL_CONF_FILE" - echo "Updated MariaDB configuration in $MYSQL_CONF_FILE" - - echo "Configuration updated successfully." - else - echo "Warning: Configuration file $MYSQL_CONF_FILE not found." - fi -done - -# Make sure all processes are killed and locks are removed before starting -kill_all_mariadb -cleanup_lock_files - -# Start MariaDB service -echo "Starting MariaDB service..." -systemctl start mariadb -sleep 5 - -# Check if MariaDB is running -if systemctl is-active --quiet mariadb; then - echo "MariaDB service is running." - - # Show some key variables to confirm changes - echo "Checking key MariaDB variables:" - echo "CPU-related settings:" - mysql -e "SHOW VARIABLES LIKE 'innodb_buffer_pool_instances';" - mysql -e "SHOW VARIABLES LIKE 'thread_pool_size';" - mysql -e "SHOW VARIABLES LIKE 'innodb_read_io_threads';" - echo "Memory-related settings:" - mysql -e "SHOW VARIABLES LIKE 'innodb_buffer_pool_size';" - mysql -e "SHOW VARIABLES LIKE 'sort_buffer_size';" - - echo "MariaDB has been optimized for FreeRADIUS workloads!" - echo "Configuration is dynamically scaled to use:" - echo "- ${BUFFER_POOL_SIZE_MB}M of RAM for buffer pool (70% of total RAM)" - echo "- ${INNODB_INSTANCES} InnoDB instances (60% of CPU cores)" - echo "- ${THREAD_POOL_SIZE} thread pool size (60% of CPU cores)" - echo "This should help resolve the FreeRADIUS SQL module issues while maintaining efficient resource usage." -else - echo "MariaDB failed to restart. Trying emergency mode..." - - # Kill all processes again - kill_all_mariadb - cleanup_lock_files - - # Try starting with skip-grant-tables - echo "Starting MariaDB with skip-grant-tables..." - systemctl set-environment MYSQLD_OPTS="--skip-grant-tables --skip-networking" - systemctl start mariadb - sleep 5 - - # Reset environment and restart normally - systemctl set-environment MYSQLD_OPTS="" - systemctl restart mariadb - sleep 5 - - if systemctl is-active --quiet mariadb; then - echo "MariaDB service is now running after emergency restart." - echo "MariaDB has been optimized for FreeRADIUS workloads!" - echo "Configuration is dynamically scaled to use:" - echo "- ${BUFFER_POOL_SIZE_MB}M of RAM for buffer pool (70% of total RAM)" - echo "- ${INNODB_INSTANCES} InnoDB instances (60% of CPU cores)" - echo "- ${THREAD_POOL_SIZE} thread pool size (60% of CPU cores)" - else - echo "MariaDB still failed to restart. Manual intervention required." - echo "Try rebooting the system and then running: sudo systemctl start mariadb" - echo "Or check the logs: sudo journalctl -u mariadb" - fi -fi - -# Function to get MySQL version -get_mysql_version() { - mysql --version | awk '{print $3}' | awk -F'-' '{print $1}' -} - -# Function to validate MySQL settings -validate_mysql_settings() { - echo "Validating MySQL settings..." - - # Check if MySQL is running - if ! systemctl is-active --quiet mariadb; then - error_exit "MariaDB service is not running" - fi - - # Check key settings - local settings=( - "innodb_buffer_pool_size" - "max_connections" - "innodb_lock_wait_timeout" - "innodb_io_capacity" - ) - - for setting in "${settings[@]}"; do - value=$(mysql -e "SHOW VARIABLES LIKE '$setting';" | awk '{print $2}') - echo "✓ $setting = $value" - done -} - -# Function to monitor MySQL performance -monitor_mysql() { - echo "Starting MySQL performance monitoring..." - - # Monitor key metrics for 60 seconds - timeout 60 mysqladmin extended-status | grep -E '(Threads_connected|Questions|Queries|Connections|Aborted_connects)' & - - echo "✓ Monitoring started. Press Ctrl+C to stop." -} - -# Validate settings -validate_mysql_settings - -# Start monitoring -monitor_mysql \ No newline at end of file From 76beb88896fc9126f44bdf681a8d08f2eb4e2af0 Mon Sep 17 00:00:00 2001 From: Barnabie Opiyo <1921395+mymanga@users.noreply.github.com> Date: Wed, 27 May 2026 15:49:28 +0300 Subject: [PATCH 14/45] Add files via upload --- ports.sh | 92 ++++++++++++++++++++++++++ template_generation.sh | 11 ++++ transfer_tmpl.sh | 145 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 248 insertions(+) create mode 100644 ports.sh create mode 100644 template_generation.sh create mode 100644 transfer_tmpl.sh diff --git a/ports.sh b/ports.sh new file mode 100644 index 0000000..380fae2 --- /dev/null +++ b/ports.sh @@ -0,0 +1,92 @@ +#!/bin/bash + +set -e + +# ----------------------------- +# Parse arguments +# ----------------------------- +while [[ $# -gt 0 ]]; do + case "$1" in + -net) + SUBNET="$2" + shift 2 + ;; + *) + echo "Usage: $0 -net " + exit 1 + ;; + esac +done + +[ -z "$SUBNET" ] && { echo "Error: -net is required"; exit 1; } + +# ----------------------------- +# Subnet handling +# ----------------------------- +BASE_IP=$(echo "$SUBNET" | cut -d/ -f1) +BASE_NET=$(echo "$BASE_IP" | awk -F. '{print $1"."$2"."$3}') + +echo "Using subnet: $SUBNET" +echo "Base network: $BASE_NET.0" + +# ----------------------------- +# Enable IPv4 forwarding +# ----------------------------- +sed -i 's/^#\?net\.ipv4\.ip_forward=.*/net.ipv4.ip_forward=1/' /etc/sysctl.conf +sysctl -w net.ipv4.ip_forward=1 >/dev/null + +# ----------------------------- +# UFW NAT configuration +# ----------------------------- +BEFORE_RULES="/etc/ufw/before.rules" + +[ ! -f "${BEFORE_RULES}.bak" ] && cp "$BEFORE_RULES" "${BEFORE_RULES}.bak" + +# Remove previous block (idempotent) +sed -i '/# BEGIN MIKROTIK NAT/,/# END MIKROTIK NAT/d' "$BEFORE_RULES" + +# Insert NAT rules +sed -i "/^\*filter/i \ +# BEGIN MIKROTIK NAT\n\ +*nat\n\ +:PREROUTING ACCEPT [0:0]\n\ +:POSTROUTING ACCEPT [0:0]\n\ +\n\ +# Masquerade Winbox return traffic\n\ +-A POSTROUTING -p tcp -d ${SUBNET} --dport 8291 -j MASQUERADE\n\ +\n\ +# DNAT rules (.2 → .10)\n\ +-A PREROUTING -p tcp --dport 2002 -j DNAT --to-destination ${BASE_NET}.2:8291\n\ +-A PREROUTING -p tcp --dport 2003 -j DNAT --to-destination ${BASE_NET}.3:8291\n\ +-A PREROUTING -p tcp --dport 2004 -j DNAT --to-destination ${BASE_NET}.4:8291\n\ +-A PREROUTING -p tcp --dport 2005 -j DNAT --to-destination ${BASE_NET}.5:8291\n\ +-A PREROUTING -p tcp --dport 2006 -j DNAT --to-destination ${BASE_NET}.6:8291\n\ +-A PREROUTING -p tcp --dport 2007 -j DNAT --to-destination ${BASE_NET}.7:8291\n\ +-A PREROUTING -p tcp --dport 2008 -j DNAT --to-destination ${BASE_NET}.8:8291\n\ +-A PREROUTING -p tcp --dport 2009 -j DNAT --to-destination ${BASE_NET}.9:8291\n\ +-A PREROUTING -p tcp --dport 2010 -j DNAT --to-destination ${BASE_NET}.10:8291\n\ +\n\ +COMMIT\n\ +# END MIKROTIK NAT\n\ +" "$BEFORE_RULES" + +# ----------------------------- +# UFW forwarding rules (quiet + safe) +# ----------------------------- +for i in {2..10}; do + ufw route allow proto tcp to ${BASE_NET}.${i} port 8291 2>/dev/null || true +done + +# ----------------------------- +# Open external ports (quiet + safe) +# ----------------------------- +for p in {2002..2010}; do + ufw allow ${p}/tcp 2>/dev/null || true +done + +# ----------------------------- +# Reload firewall +# ----------------------------- +ufw reload >/dev/null + +echo "✔ UFW NAT + forwarding configured successfully (.2 → .10)" diff --git a/template_generation.sh b/template_generation.sh new file mode 100644 index 0000000..1f62a41 --- /dev/null +++ b/template_generation.sh @@ -0,0 +1,11 @@ +qm create 9013 --name ubuntu2604resolute --cores 2 --memory 2048 --net0 virtio,bridge=vmbr1,tag=20 --scsihw virtio-scsi-pci + +qm set 9013 --scsi0 simplux_nbo1:0,import-from=/root/resolute-server-cloudimg-amd64.img + +qm set 9013 --ide2 simplux_nbo1:cloudinit + +qm set 9013 --boot order=scsi0 + +qm set 9013 --serial0 socket --vga serial0 + +qm template 9013 diff --git a/transfer_tmpl.sh b/transfer_tmpl.sh new file mode 100644 index 0000000..4adc190 --- /dev/null +++ b/transfer_tmpl.sh @@ -0,0 +1,145 @@ +#!/bin/bash + +# Check for jq and install if missing +if ! command -v jq &> /dev/null; then + echo "Installing jq..." + apt-get update && apt-get install -y jq +fi + +# API credentials +API_HOST="https://dash.simplux.xyz:8006" +API_TOKEN="root@pam!auto-billing=dc39fabe-c253-48f1-84e4-e07d2168563d" + +# Template mapping (source_id:template_name) +declare -A templates=( + [9009]="debian11bullseye" + [9010]="debian12bookworm" + [9011]="debian13trixie" + [9015]="fedora37" + [9016]="fedora38" + [9017]="rocky8" + [9018]="rocky9" + [9019]="alpine319" +) + +# Source and destination nodes +SOURCE_NODE="s1" +DEST_NODES=("s2" "s3") + +# Starting VMID for clones +NEXT_ID=9100 + +# Function to check if template exists by name +template_exists() { + local node="$1" + local template_name="$2" + + # Get list of all VMs on the node + local vms=$(call_api "/nodes/${node}/qemu" "GET") + + # Check if any VM has matching name and is a template + echo "$vms" | jq -r '.data[] | select(.template == 1) | .name' | grep -q "^${template_name}$" + return $? +} + +# Function to make API calls +call_api() { + local endpoint="$1" + local method="$2" + local data="$3" + + if [ -n "$data" ]; then + curl -k -s -X "$method" \ + -H "Authorization: PVEAPIToken=$API_TOKEN" \ + -H "Content-Type: application/json" \ + -d "$data" \ + "${API_HOST}/api2/json${endpoint}" + else + curl -k -s -X "$method" \ + -H "Authorization: PVEAPIToken=$API_TOKEN" \ + "${API_HOST}/api2/json${endpoint}" + fi +} + +# Function to wait for task completion +wait_for_task() { + local node="$1" + local task_id="$2" + local max_attempts=60 + local attempt=0 + + echo "Waiting for task $task_id to complete..." + while [ $attempt -lt $max_attempts ]; do + local status=$(call_api "/nodes/${node}/tasks/${task_id}/status" "GET" | jq -r '.data.status') + + if [ "$status" = "stopped" ]; then + local exitstatus=$(call_api "/nodes/${node}/tasks/${task_id}/status" "GET" | jq -r '.data.exitstatus') + if [ "$exitstatus" = "OK" ]; then + echo "Task completed successfully" + return 0 + else + echo "Task failed with status: $exitstatus" + return 1 + fi + fi + + attempt=$((attempt + 1)) + sleep 10 + done + + echo "Timeout waiting for task completion" + return 1 +} + +# Main script +echo "Starting template transfer process..." + +# Process each template +for source_vmid in "${!templates[@]}"; do + template_name="${templates[$source_vmid]}" + echo "Processing template: $template_name (Source VMID: $source_vmid)" + + # Clone to each destination node + for dest_node in "${DEST_NODES[@]}"; do + # Check if template already exists on destination + if template_exists "$dest_node" "$template_name"; then + echo "Template $template_name already exists on $dest_node, skipping..." + continue + fi + + echo "Cloning to node $dest_node with new ID: $NEXT_ID..." + + # Create full clone with new ID + response=$(call_api "/nodes/${SOURCE_NODE}/qemu/${source_vmid}/clone" "POST" \ + "{\"newid\": \"${NEXT_ID}\", \"target\": \"${dest_node}\", \"full\": 1, \"name\": \"${template_name}\"}") + + # Extract task ID + task_id=$(echo "$response" | jq -r '.data') + + if [ -n "$task_id" ] && [ "$task_id" != "null" ]; then + # Wait for clone to complete + if wait_for_task "$SOURCE_NODE" "$task_id"; then + echo "Successfully cloned $template_name to $dest_node as VMID $NEXT_ID" + # Convert the cloned VM to template + convert_response=$(call_api "/nodes/${dest_node}/qemu/${NEXT_ID}/template" "POST") + echo "Converting to template on $dest_node..." + if [[ $(echo "$convert_response" | jq -r '.data') == "null" ]]; then + echo "Successfully converted VMID $NEXT_ID to template on $dest_node" + else + echo "Failed to convert VMID $NEXT_ID to template on $dest_node" + echo "API Response: $convert_response" + fi + else + echo "Failed to clone $template_name to $dest_node" + fi + else + echo "Failed to start clone task for $template_name to $dest_node" + echo "API Response: $response" + fi + + # Increment ID for next clone + NEXT_ID=$((NEXT_ID + 1)) + done +done + +echo "Template transfer process completed" From cd20af875058413ef10f1130bdee81133e835577 Mon Sep 17 00:00:00 2001 From: Barnabie Opiyo Date: Wed, 27 May 2026 23:34:23 +0300 Subject: [PATCH 15/45] optimzed versions of universal, simplespot and simpleisp --- simpleisp.sh | 1073 +++++++++++++++++++++++++++++++++++++++++++++++++ simplespot.sh | 1073 +++++++++++++++++++++++++++++++++++++++++++++++++ universal.sh | 55 ++- 3 files changed, 2197 insertions(+), 4 deletions(-) create mode 100644 simpleisp.sh create mode 100644 simplespot.sh diff --git a/simpleisp.sh b/simpleisp.sh new file mode 100644 index 0000000..240ba66 --- /dev/null +++ b/simpleisp.sh @@ -0,0 +1,1073 @@ +#!/bin/bash + +# Setup logging and error handling +INSTALL_LOG="/root/install.txt" +STEP_COUNT=0 +COMPLETED_STEPS=() + +# Get server hostname and set email +DOMAIN=$(hostname -f) +EMAIL_ADDRESS="simpluxsolutions@gmail.com" + +# Configuration variables - ONLY DIFFERENCES BETWEEN SCRIPTS +GITHUB_REPO_URL="https://github.com/simpleisp/radius.git" +PHP_VERSION="7.4" + +# Logging functions +log_info() { echo "ℹ️ INFO: $1" | tee -a "$INSTALL_LOG"; } +log_success() { echo "✅ SUCCESS: $1" | tee -a "$INSTALL_LOG"; } +log_error() { echo "❌ ERROR: $1" | tee -a "$INSTALL_LOG"; } +log_step() { STEP_COUNT=$((STEP_COUNT + 1)); echo "👉 STEP $STEP_COUNT: $1" | tee -a "$INSTALL_LOG"; } + +handle_error() { + log_error "$1" + echo -e "\nCompleted steps before failure:" + printf '%s\n' "${COMPLETED_STEPS[@]}" + echo -e "\nCheck $INSTALL_LOG for more details" + exit 1 +} + +# Initialize log file +touch "$INSTALL_LOG" || { echo "Cannot create log file"; exit 1; } +echo "SimpleISP Installation Log - $(date '+%Y-%m-%d %H:%M:%S')" > "$INSTALL_LOG" +echo "----------------------------------------" >> "$INSTALL_LOG" + +# Configure system for Valkey (memory overcommit and other optimizations) +log_step "Configuring system for Valkey" + +# Enable memory overcommit +if ! grep -q "^vm.overcommit_memory" /etc/sysctl.conf; then + echo "vm.overcommit_memory = 1" | tee -a /etc/sysctl.conf + sysctl -p /etc/sysctl.conf + log_info "Enabled memory overcommit in sysctl" +else + log_info "Memory overcommit already configured in sysctl" +fi +COMPLETED_STEPS+=("System configured for Valkey") + +# Check for cleanup marker file +CLEANUP_MARKER="/root/.simpleisp_cleanup_done" +REINSTALL=false + +if [ -f "$CLEANUP_MARKER" ]; then + log_info "Detected previous cleanup ($(cat $CLEANUP_MARKER))" + log_info "Forcing reinstallation of critical directories and files" + REINSTALL=true + + # Remove the marker file after handling it + rm -f "$CLEANUP_MARKER" + log_success "Cleanup marker processed and removed" +fi + +# Ensure script runs as root +log_step "Checking root privileges" +if [ "$EUID" -ne 0 ]; then handle_error "Please run as root"; fi +COMPLETED_STEPS+=("Root check passed") + +# Get Ubuntu version +log_step "Detecting Ubuntu version" +UBUNTU_VERSION=$(lsb_release -cs) || handle_error "Failed to detect Ubuntu version" +log_info "Detected Ubuntu version: $UBUNTU_VERSION" +COMPLETED_STEPS+=("Ubuntu version detected: $UBUNTU_VERSION") + +# Set PHP Repo for Ubuntu 24.04 (Noble) +log_step "Adding PHP repository" +if [ "$UBUNTU_VERSION" = "noble" ]; then + # Set up PHP repository for Ubuntu 24.04 + gpgKey='B8DC7E53946656EFBCE4C1DD71DAEAAB4AD4CAB6' + gpgKeyPath='/etc/apt/keyrings/ondrej-ubuntu-php.gpg' + gpgURL="https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x${gpgKey}" + + # Create keyrings directory if it doesn't exist + install -d -m 0755 /etc/apt/keyrings || handle_error "Failed to create keyrings directory" + + # Download and set up GPG key + curl "${gpgURL}" | gpg --dearmor | tee ${gpgKeyPath} >/dev/null || handle_error "Failed to setup PHP GPG key" + gpg --dry-run --quiet --import --import-options import-show ${gpgKeyPath} + + # Create the sources file for PHP repository + cat > /etc/apt/sources.list.d/ondrej-ubuntu-php-noble.sources << 'EOL' +Types: deb +URIs: https://ppa.launchpadcontent.net/ondrej/php/ubuntu/ +Suites: noble +Components: main +Signed-By: /etc/apt/keyrings/ondrej-ubuntu-php.gpg +EOL +else + # For other Ubuntu versions, use the traditional PPA method + LC_ALL=C.UTF-8 add-apt-repository ppa:ondrej/php -y || handle_error "Failed to add PHP repository" +fi +COMPLETED_STEPS+=("PHP repository added") + +# Set NetworkRADIUS PGP public key +log_step "Configuring NetworkRADIUS repository" +install -d -o root -g root -m 0755 /etc/apt/keyrings || handle_error "Failed to create keyrings directory" +curl -s 'https://packages.networkradius.com/pgp/packages%40networkradius.com' | sudo tee /etc/apt/keyrings/packages.networkradius.com.asc > /dev/null || handle_error "Failed to download NetworkRADIUS PGP key" +COMPLETED_STEPS+=("NetworkRADIUS PGP key installed") + +# Add NetworkRADIUS APT preferences +printf 'Package: /freeradius/\nPin: origin "packages.networkradius.com"\nPin-Priority: 999\n' | sudo tee /etc/apt/preferences.d/networkradius > /dev/null || handle_error "Failed to set NetworkRADIUS preferences" + +# Add NetworkRADIUS repository based on Ubuntu version +case $UBUNTU_VERSION in + "noble") REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/noble noble main" ;; + "jammy") REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/jammy jammy main" ;; + "focal") REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/focal focal main" ;; + *) handle_error "Unsupported Ubuntu version: $UBUNTU_VERSION" ;; +esac + +echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/packages.networkradius.com.asc] $REPO_URL" | sudo tee /etc/apt/sources.list.d/networkradius.list > /dev/null || handle_error "Failed to add NetworkRADIUS repository" +log_success "NetworkRADIUS repository configured for Ubuntu $UBUNTU_VERSION" +COMPLETED_STEPS+=("NetworkRADIUS repository configured") + +# Set environment variable to avoid interactive prompts +export DEBIAN_FRONTEND=noninteractive + +# Update and upgrade system +log_step "Updating system packages" +apt-get update || handle_error "Failed to update package lists" +apt-get upgrade -y || handle_error "Failed to upgrade packages" +COMPLETED_STEPS+=("System packages updated") + +# Install required packages (Cleaned virtual PHP packages and freeradius-rest) +log_step "Installing required packages" +INSTALL_PACKAGES="nginx-full python3-certbot-nginx php${PHP_VERSION}-fpm php${PHP_VERSION}-mysql php${PHP_VERSION}-cli php${PHP_VERSION}-curl php${PHP_VERSION}-zip php${PHP_VERSION}-common php${PHP_VERSION}-gd php${PHP_VERSION}-mbstring php${PHP_VERSION}-xml php${PHP_VERSION}-dev php${PHP_VERSION}-bcmath php${PHP_VERSION}-intl php${PHP_VERSION}-redis git unzip curl wget software-properties-common apt-transport-https ca-certificates gnupg lsb-release supervisor valkey-server valkey-tools valkey-redis-compat valkey-sentinel ufw openvpn easy-rsa freeradius freeradius-mysql freeradius-utils mariadb-server mariadb-client" + +if [ "$REINSTALL" = true ]; then + log_info "Reinstalling packages (forcing configuration file replacement)" + apt-get install --reinstall -y -o Dpkg::Options::="--force-confmiss" $INSTALL_PACKAGES || handle_error "Failed to reinstall packages" +else + apt-get install -y $INSTALL_PACKAGES || handle_error "Failed to install packages" +fi +COMPLETED_STEPS+=("Required packages installed") + +# Configure Valkey service overrides +log_step "Configuring Valkey service overrides" +VKEY_OVERRIDE_DIR="/etc/systemd/system/valkey.service.d" +VKEY_OVERRIDE_FILE="${VKEY_OVERRIDE_DIR}/override.conf" + +mkdir -p "$VKEY_OVERRIDE_DIR" +cat > "$VKEY_OVERRIDE_FILE" << 'EOF' +[Unit] + +[Service] +# Increase timeouts to prevent premature termination +TimeoutStartSec=300 +TimeoutStopSec=300 + +# Ensure service restarts on failure +Restart=always +RestartSec=10s + +# Disable OOM killer for Valkey +OOMScoreAdjust=-1000 +EOF + +# Apply changes and restart Valkey +log_info "Applying Valkey service configuration..." + +# Configure Valkey with optimized settings for FreeRADIUS +log_step "Configuring Valkey with optimized settings" + +# Calculate optimal memory allocation (Safe 15% of available RAM, capped at 3GB, minimum 1GB) +TOTAL_RAM_KB=$(grep MemTotal /proc/meminfo | awk '{print $2}') +TOTAL_RAM_MB=$((TOTAL_RAM_KB / 1024)) +MAX_MEMORY_MB=$((TOTAL_RAM_MB * 15 / 100)) +if [ "$MAX_MEMORY_MB" -gt 3072 ]; then MAX_MEMORY_MB=3072; fi +if [ "$MAX_MEMORY_MB" -lt 1024 ]; then MAX_MEMORY_MB=1024; fi + +log_info "Configuring Valkey with ${MAX_MEMORY_MB}MB memory allocation" + +# Create Valkey configuration directory if it doesn't exist +mkdir -p /etc/valkey || handle_error "Failed to create Valkey configuration directory" + +# Configure Valkey with optimized settings for FreeRADIUS +cat > /etc/valkey/valkey.conf << EOL +# Valkey configuration for FreeRADIUS +bind 0.0.0.0 ::0 +protected-mode yes +port 6379 +tcp-backlog 511 +timeout 0 +tcp-keepalive 300 +daemonize no +supervised systemd +pidfile /var/run/valkey/valkey.pid +loglevel notice +logfile /var/log/valkey/valkey.log +databases 16 + +# Memory management +maxmemory ${MAX_MEMORY_MB}mb +maxmemory-policy volatile-lru +maxmemory-samples 5 + +# AOF persistence (enabled for better durability) +appendonly yes +dir /var/lib/valkey +appendfilename "appendonly.aof" +appendfsync everysec +no-appendfsync-on-rewrite no +auto-aof-rewrite-percentage 100 +auto-aof-rewrite-min-size 64mb +aof-load-truncated yes +aof-rewrite-incremental-fsync yes + +# Performance optimizations +stop-writes-on-bgsave-error no +rdbcompression yes +rdbchecksum yes +dbfilename dump.rdb + +# Disable RDB snapshots since we're using AOF +save "" + +# Security (Reuse the same password as MySQL for simplicity) +requirepass "$MYSQL_PASSWORD" + +# Network +tcp-keepalive 300 +repl-timeout 60 +repl-ping-slave-period 10 +repl-backlog-size 1mb +repl-backlog-ttl 3600 +timeout 0 +tcp-keepalive 300 + +# Disable dangerous commands +rename-command FLUSHDB "" +rename-command FLUSHALL "" +rename-command CONFIG "" +rename-command SHUTDOWN "" + +# Tune threads and clients +io-threads 2 +io-threads-do-reads yes +maxclients 10000 + +# Tune data structures +active-expire-effort 1 +client-output-buffer-limit normal 0 0 0 +client-output-buffer-limit replica 256mb 64mb 60 +client-output-buffer-limit pubsub 32mb 8mb 60 +hash-max-ziplist-entries 512 +hash-max-ziplist-value 64 +list-max-ziplist-size -2 +set-max-intset-entries 512 +zset-max-ziplist-entries 128 +zset-max-ziplist-value 64 +hll-sparse-max-bytes 3000 +stream-node-max-bytes 4096 +stream-node-max-entries 100 + +# Enable active defragmentation +active-defrag-threshold-lower 10 +active-defrag-threshold-upper 100 +active-defrag-ignore-bytes 100mb +active-defrag-cycle-min 5 +active-defrag-cycle-max 75 +active-defrag-max-scan-fields 1000 +EOL + +# Restart Valkey to apply new configuration +systemctl daemon-reexec || log_warning "daemon-reexec failed (non-critical)" +systemctl daemon-reload || handle_error "Failed to reload systemd daemon" +systemctl restart valkey-server || handle_error "Failed to restart Valkey" +systemctl enable valkey-server || handle_error "Failed to enable Valkey" + +# Verify Valkey is running +log_step "Verifying Valkey service status" +if systemctl is-active --quiet valkey-server; then + log_success "Valkey service is running" + COMPLETED_STEPS+=("Valkey configured with ${MAX_MEMORY_MB}MB memory allocation") +else + log_warning "Valkey service is not running as expected. Checking status..." + systemctl status valkey-server --no-pager || true + + log_info "Attempting to start Valkey service..." + if systemctl start valkey-server; then + log_success "Successfully started Valkey service" + COMPLETED_STEPS+=("Valkey configured with ${MAX_MEMORY_MB}MB memory allocation") + else + log_error "Failed to start Valkey service. Please check the logs with: journalctl -u valkey-server -n 50" + log_warning "Continuing installation despite Valkey service issue..." + COMPLETED_STEPS+=("Valkey configuration completed but service failed to start") + fi +fi + +# Create Valkey debug script +log_step "Creating Valkey debug script" +cat > /usr/local/bin/valkey-debug.sh << 'EOF' +#!/bin/bash + +VALKEY_HOST="127.0.0.1" +VALKEY_PORT="6379" + +echo "=== Valkey Status ===" +systemctl status valkey --no-pager -l + +echo -e "\n=== Valkey Key Statistics ===" +echo "Total Keys in DB 0: $(valkey-cli -h $VALKEY_HOST -p $VALKEY_PORT dbsize)" +EOF + +chmod +x /usr/local/bin/valkey-debug.sh || handle_error "Failed to make Valkey debug script executable" +COMPLETED_STEPS+=("Valkey monitoring configured") + +# Add monitoring cron job (Safe Append Fix) +log_step "Adding monitoring cron job" +(crontab -l 2>/dev/null; echo "*/5 * * * * /usr/local/bin/valkey-debug.sh") | crontab - || handle_error "Failed to install monitoring cron job" +COMPLETED_STEPS+=("Monitoring cron job added") + +# Verify Valkey is working +log_step "Verifying Valkey installation" +if ! systemctl is-active --quiet valkey; then handle_error "Valkey service is not running"; fi + +# Test Valkey connectivity and basic operations +if [ "$(valkey-cli ping)" != "PONG" ]; then handle_error "Valkey is not responding to ping"; fi +if [ "$(valkey-cli set test_key test_value)" != "OK" ]; then handle_error "Valkey write operation failed"; fi +if [ "$(valkey-cli get test_key)" != "test_value" ]; then handle_error "Valkey read operation failed"; fi +if [ "$(valkey-cli del test_key)" != "1" ]; then handle_error "Valkey delete operation failed"; fi +if ! valkey-cli info | grep -q "valkey_version"; then handle_error "Unable to get Valkey server information"; fi +COMPLETED_STEPS+=("Valkey functionality verified") + +# Set Default PHP Version +log_step "Setting default PHP version" +update-alternatives --set php /usr/bin/php${PHP_VERSION} || handle_error "Failed to set default PHP version" +COMPLETED_STEPS+=("PHP ${PHP_VERSION} set as default") + +# Install and configure ionCube Loader +log_step "Installing ionCube Loader" +if [ -d "/usr/local/ioncube" ] && [ -f "/usr/local/ioncube/ioncube_loader_lin_${PHP_VERSION}.so" ]; then + log_info "ionCube Loader already exists, skipping download and installation" + COMPLETED_STEPS+=("ionCube Loader reused (already exists)") +else + log_info "ionCube Loader not found, downloading and installing" + cd /tmp || handle_error "Failed to change to /tmp directory" + wget -O ioncube.zip "https://downloads.ioncube.com/loader_downloads/ioncube_loaders_lin_x86-64.zip" || handle_error "Failed to download ionCube" + unzip -q ioncube.zip || handle_error "Failed to extract ionCube" + rm -rf /usr/local/ioncube 2>/dev/null + mv ioncube /usr/local/ || handle_error "Failed to move ionCube to /usr/local" + COMPLETED_STEPS+=("ionCube Loader downloaded and installed") +fi + +# Create ionCube ini file with absolute path +cat > /etc/php/${PHP_VERSION}/mods-available/ioncube.ini << EOL +zend_extension = /usr/local/ioncube/ioncube_loader_lin_${PHP_VERSION}.so +EOL + +# Enable ionCube for PHP CLI and FPM +ln -sf /etc/php/${PHP_VERSION}/mods-available/ioncube.ini /etc/php/${PHP_VERSION}/cli/conf.d/00-ioncube.ini || handle_error "Failed to enable ionCube for CLI" +ln -sf /etc/php/${PHP_VERSION}/mods-available/ioncube.ini /etc/php/${PHP_VERSION}/fpm/conf.d/00-ioncube.ini || handle_error "Failed to enable ionCube for FPM" +systemctl restart php${PHP_VERSION}-fpm || handle_error "Failed to restart PHP-FPM" + +# Verify ionCube installation +if php -v | grep -q "ionCube PHP Loader"; then + log_success "ionCube Loader installed and enabled successfully" + COMPLETED_STEPS+=("ionCube Loader installed and configured") +else + handle_error "ionCube Loader installation verification failed" +fi + +# Start and enable MariaDB +log_step "Configuring MariaDB" +if [ ! -d "/var/lib/mysql/mysql" ]; then + log_info "Initializing MariaDB system database" + mysql_install_db --user=mysql --datadir=/var/lib/mysql || handle_error "Failed to initialize MariaDB" +fi + +systemctl start mariadb || handle_error "Failed to start MariaDB" +systemctl enable mariadb || handle_error "Failed to enable MariaDB" + +# Ensure debian-start script exists (recreate if missing) +if [ ! -f "/etc/mysql/debian-start" ]; then + log_info "Creating missing /etc/mysql/debian-start script" + cat > /etc/mysql/debian-start << 'EOF' +#!/bin/bash +if [ "$(id -u)" != "0" ]; then echo "This script must be run as root" 1>&2; exit 1; fi +if ! /usr/bin/mysqladmin --defaults-file=/etc/mysql/debian.cnf ping > /dev/null 2>&1; then exit 0; fi +exit 0 +EOF + chmod +x /etc/mysql/debian-start || handle_error "Failed to make debian-start executable" + log_success "Created /etc/mysql/debian-start script" +fi +COMPLETED_STEPS+=("MariaDB initialized, started and enabled") + +# Configure MySQL to allow remote connections +log_step "Configuring MySQL for remote connections" +mkdir -p /etc/mysql/mariadb.conf.d/ || handle_error "Failed to create MariaDB conf directory" + +cat > /etc/mysql/mariadb.conf.d/50-server.cnf << 'EOL' +[server] +[mariadbd] +user = mysql +pid-file = /run/mysqld/mysqld.pid +basedir = /usr +datadir = /var/lib/mysql +tmpdir = /tmp +skip-external-locking +bind-address = 0.0.0.0 +key_buffer_size = 16M +max_allowed_packet = 16M +thread_stack = 192K +thread_cache_size = 8 +myisam-recover-options = BACKUP +query_cache_limit = 1M +query_cache_size = 16M +expire_logs_days = 10 +max_binlog_size = 100M +character-set-server = utf8mb4 +collation-server = utf8mb4_general_ci +innodb_buffer_pool_size = 1G +innodb_log_file_size = 256M +innodb_flush_method = O_DIRECT +innodb_flush_log_at_trx_commit = 2 +innodb_file_per_table = 1 +[embedded] +[mariadb] +EOL + +systemctl restart mariadb || handle_error "Failed to restart MariaDB after configuration change" +COMPLETED_STEPS+=("MySQL configured for remote connections") + +# Generate random credentials or reuse existing ones +DB_CREDENTIALS_FILE="/root/db.txt" +if [ -f "$DB_CREDENTIALS_FILE" ] && [ -r "$DB_CREDENTIALS_FILE" ]; then + log_step "Found existing database credentials, reusing them" + MYSQL_USER=$(grep "^DB_USERNAME=" "$DB_CREDENTIALS_FILE" | cut -d'=' -f2) + MYSQL_PASSWORD=$(grep "^DB_PASSWORD=" "$DB_CREDENTIALS_FILE" | cut -d'=' -f2) + MYSQL_DATABASE=$(grep "^DB_DATABASE=" "$DB_CREDENTIALS_FILE" | cut -d'=' -f2) + + if [ -n "$MYSQL_USER" ] && [ -n "$MYSQL_PASSWORD" ] && [ -n "$MYSQL_DATABASE" ]; then + log_info "Reusing existing database credentials: User=$MYSQL_USER, Database=$MYSQL_DATABASE" + COMPLETED_STEPS+=("Database credentials reused from existing file") + else + log_info "Existing db.txt file is incomplete, generating new credentials" + MYSQL_USER="user_$(openssl rand -hex 3)" + MYSQL_PASSWORD="$(openssl rand -base64 12)" + MYSQL_DATABASE="radius" + COMPLETED_STEPS+=("New database credentials generated (existing file was incomplete)") + fi +else + log_step "No existing database credentials found, generating new ones" + MYSQL_USER="user_$(openssl rand -hex 3)" + MYSQL_PASSWORD="$(openssl rand -base64 12)" + MYSQL_DATABASE="radius" + COMPLETED_STEPS+=("New database credentials generated") +fi + +# Save database credentials +log_step "Saving database credentials" +echo "MySQL Credentials:" > "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" +echo "DB_HOST=localhost" >> "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" +echo "DB_PORT=3306" >> "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" +echo "DB_DATABASE=$MYSQL_DATABASE" >> "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" +echo "DB_USERNAME=$MYSQL_USER" >> "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" +echo "DB_PASSWORD=$MYSQL_PASSWORD" >> "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" +COMPLETED_STEPS+=("Database credentials saved") + +# Secure MariaDB installation +log_step "Securing MariaDB installation" +mysql -e "DELETE FROM mysql.user WHERE User='';" || handle_error "Failed to delete anonymous MariaDB user" +mysql -e "DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');" || handle_error "Failed to delete remote MariaDB root user" +mysql -e "DROP DATABASE IF EXISTS test;" || handle_error "Failed to delete test database" +mysql -e "DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%';" || handle_error "Failed to delete test database" +mysql -e "FLUSH PRIVILEGES;" || handle_error "Failed to flush MariaDB privileges" +COMPLETED_STEPS+=("MariaDB installation secured") + +# Create database and user +log_step "Creating database and user" +mysql -e "CREATE DATABASE $MYSQL_DATABASE;" || handle_error "Failed to create database" +mysql -e "CREATE USER '$MYSQL_USER'@'%' IDENTIFIED BY '$MYSQL_PASSWORD';" || handle_error "Failed to create database user" +mysql -e "GRANT ALL PRIVILEGES ON $MYSQL_DATABASE.* TO '$MYSQL_USER'@'%';" || handle_error "Failed to grant database privileges" +mysql -e "FLUSH PRIVILEGES;" || handle_error "Failed to flush MariaDB privileges" +COMPLETED_STEPS+=("Database and user created with full access") + +# Install Composer +log_step "Installing Composer" +if ! command -v composer &> /dev/null; then + php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" || handle_error "Failed to download Composer installer" + php composer-setup.php --quiet || handle_error "Failed to install Composer" + rm composer-setup.php || handle_error "Failed to remove Composer installer" + mv composer.phar /usr/local/bin/composer || handle_error "Failed to move Composer to /usr/local/bin" + chmod +x /usr/local/bin/composer || handle_error "Failed to make Composer executable" +fi +COMPLETED_STEPS+=("Composer installed") + +# Configure Nginx +log_step "Configuring Nginx" +mv /etc/nginx/sites-available/default /etc/nginx/sites-available/default.bak || handle_error "Failed to backup default Nginx site" +touch /etc/nginx/sites-available/default || handle_error "Failed to create new Nginx site" +ln -sf /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default || handle_error "Failed to symlink Nginx site" + +cat > /etc/nginx/sites-available/default << EOL +server { + listen 80; + listen [::]:80; + + root /var/www/html/public; + index index.php index.html index.htm index.nginx-debian.html; + + server_name $DOMAIN; + + location / { + try_files \$uri \$uri/ /index.php?\$query_string; + } + + location ~ \.php$ { + include snippets/fastcgi-php.conf; + fastcgi_pass unix:/var/run/php/php${PHP_VERSION}-fpm.sock; + } + + location ~ /\.ht { + deny all; + } +} +EOL +COMPLETED_STEPS+=("Nginx configured") + +# Configure SSL with Certbot +log_step "Configuring SSL with Certbot" +echo "Configuring SSL certificate for $DOMAIN" + +if [ -d "/etc/letsencrypt/live/$DOMAIN" ]; then + log_info "SSL certificate already exists for $DOMAIN, reusing existing certificate" + echo -e "1\n" | certbot --nginx -d "$DOMAIN" || handle_error "Failed to reuse SSL certificate" + COMPLETED_STEPS+=("SSL certificate reused (already exists)") +else + log_info "No existing SSL certificate found for $DOMAIN, requesting new certificate" + certbot --nginx -d "$DOMAIN" --agree-tos --email "$EMAIL_ADDRESS" --no-eff-email --non-interactive --redirect || handle_error "Failed to configure SSL with Certbot" + COMPLETED_STEPS+=("SSL configured with Certbot") +fi + +# Test the Nginx configuration (Graceful restart applied) +log_step "Restarting Nginx" +nginx -t || handle_error "Nginx configuration failed" +systemctl restart nginx || handle_error "Failed to restart Nginx" +COMPLETED_STEPS+=("Nginx restarted gracefully") + +# Setup Laravel application +log_step "Setting up Laravel application" +LOCAL_PATH="/var/www/html" +REPO_URL="$GITHUB_REPO_URL" + +if [ -d "$LOCAL_PATH" ]; then + rm -rf "$LOCAL_PATH" || handle_error "Failed to remove existing web root" +fi + +git clone -b master "$REPO_URL" "$LOCAL_PATH" || handle_error "Failed to clone repository" +cd "$LOCAL_PATH" || handle_error "Failed to change directory to web root" + +# Install Laravel dependencies +log_step "Installing Laravel dependencies" +composer install --no-interaction || handle_error "Failed to install Laravel dependencies" +COMPLETED_STEPS+=("Laravel dependencies installed") + +# Create and configure .env file +log_step "Configuring .env file" +cp .env.example .env || handle_error "Failed to copy .env.example to .env" +php artisan key:generate --force || handle_error "Failed to generate Laravel key" +COMPLETED_STEPS+=(".env file configured") + +# Update .env with database credentials +sed -i "s|DB_HOST=.*|DB_HOST=localhost|" .env || handle_error "Failed to update DB_HOST in .env" +sed -i "s|DB_PORT=.*|DB_PORT=3306|" .env || handle_error "Failed to update DB_PORT in .env" +sed -i "s|DB_DATABASE=.*|DB_DATABASE=$MYSQL_DATABASE|" .env || handle_error "Failed to update DB_DATABASE in .env" +sed -i "s|DB_USERNAME=.*|DB_USERNAME=$MYSQL_USER|" .env || handle_error "Failed to update DB_USERNAME in .env" +sed -i "s|DB_PASSWORD=.*|DB_PASSWORD=$MYSQL_PASSWORD|" .env || handle_error "Failed to update DB_PASSWORD in .env" +sed -i "s|APP_URL=.*|APP_URL=https://$DOMAIN|" .env || handle_error "Failed to update APP_URL in .env" +sed -i "s|APP_NAME=.*|APP_NAME=\"$DOMAIN\"|" .env || handle_error "Failed to update APP_NAME in .env" +COMPLETED_STEPS+=(".env file updated with database credentials") + +# Run Laravel migrations and seed the database +log_step "Running Laravel migrations and seeding database" +php artisan migrate --force || handle_error "Failed to run Laravel migrations" +php artisan db:seed --force || handle_error "Failed to seed database" +COMPLETED_STEPS+=("Laravel migrations run and database seeded") + +# Set correct www permissions +log_step "Setting correct www permissions" +chown -R www-data:www-data /var/www/html || handle_error "Failed to set ownership of web root" +chmod -R 775 /var/www/html/storage || handle_error "Failed to set permissions of storage directory" +chmod -R 775 /var/www/html/bootstrap/cache || handle_error "Failed to set permissions of cache directory" + +# Make the scripts executable +chmod +x /var/www/html/sh/set_permissions.sh || handle_error "Failed to make permissions script executable" +chmod +x /var/www/html/sh/restart-services.sh || handle_error "Failed to make services script executable" + +# Run the script once to apply initial configuration +/var/www/html/sh/set_permissions.sh || handle_error "Failed to run permissions script" +/var/www/html/sh/restart-services.sh || handle_error "Failed to run services script" +COMPLETED_STEPS+=("Correct www permissions set") + +# Optimize RADIUS database indexes +log_step "Optimizing RADIUS database indexes" +cat > /tmp/radius_optimize.sql << "EOL" +USE radius; +ALTER TABLE radcheck ADD INDEX idx_username (username), ADD INDEX idx_attribute (attribute); +ANALYZE TABLE radcheck; +ALTER TABLE radreply ADD INDEX idx_username (username), ADD INDEX idx_attribute (attribute); +ANALYZE TABLE radreply; +ALTER TABLE radusergroup ADD INDEX idx_username (username), ADD INDEX idx_groupname (groupname); +ANALYZE TABLE radusergroup; +ALTER TABLE radgroupcheck ADD INDEX idx_groupname (groupname), ADD INDEX idx_attribute (attribute); +ANALYZE TABLE radgroupcheck; +ALTER TABLE radgroupreply ADD INDEX idx_groupname (groupname), ADD INDEX idx_attribute (attribute); +ANALYZE TABLE radgroupreply; +ALTER TABLE radacct ADD INDEX idx_username (username), ADD INDEX idx_acctsessionid (acctsessionid), ADD INDEX idx_framedipaddress (framedipaddress), ADD INDEX idx_acctstarttime (acctstarttime), ADD INDEX idx_acctstoptime (acctstoptime), ADD INDEX idx_nasipaddress (nasipaddress), ADD INDEX idx_calledstationid (calledstationid), ADD INDEX idx_callingstationid (callingstationid); +ANALYZE TABLE radacct; +ALTER TABLE radpostauth ADD INDEX idx_username (username), ADD INDEX idx_reply (reply), ADD INDEX idx_authdate (authdate); +ANALYZE TABLE radpostauth; +ALTER TABLE radcheck ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE radreply ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE radusergroup ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE radgroupcheck ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE radgroupreply ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE radacct ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE radpostauth ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ANALYZE TABLE radcheck; +ANALYZE TABLE radreply; +ANALYZE TABLE radusergroup; +ANALYZE TABLE radgroupcheck; +ANALYZE TABLE radgroupreply; +ANALYZE TABLE radacct; +ANALYZE TABLE radpostauth; +EOL + +mysql -u root < /tmp/radius_optimize.sql || handle_error "Failed to optimize RADIUS database indexes" +rm -f /tmp/radius_optimize.sql || handle_error "Failed to remove radius optimization sql file" +COMPLETED_STEPS+=("RADIUS database indexes optimized") + +# Configure Supervisor for queue worker (Pre-created log dir fix) +log_step "Configuring Supervisor for queue worker" +mkdir -p /var/www/html/storage/logs || handle_error "Failed to create Laravel logs directory" +cat > /etc/supervisor/conf.d/queue-worker.conf << "EOL" +[program:queue-worker] +process_name=%(program_name)s_%(process_num)02d +command=php /var/www/html/artisan queue:work --tries=3 +autostart=true +autorestart=true +stopasgroup=true +killasgroup=true +user=www-data +numprocs=5 +redirect_stderr=true +stdout_logfile=/var/www/html/storage/logs/queue-worker.log +EOL +COMPLETED_STEPS+=("Supervisor configured for queue worker") + +# Install OpenVPN based on Ubuntu version +log_step "Installing OpenVPN" +case $UBUNTU_VERSION in + "focal"|"jammy"|"noble") + echo "Installing OpenVPN for Ubuntu $UBUNTU_VERSION" + export AUTO_INSTALL=y + curl -O https://raw.githubusercontent.com/simpleisp/bash/main/openvpn.sh || handle_error "Failed to download OpenVPN installer" + chmod +x openvpn.sh || handle_error "Failed to make OpenVPN installer executable" + ./openvpn.sh || handle_error "Failed to install OpenVPN" + + # Set more secure permissions for OpenVPN + chown -R root:root /etc/openvpn || handle_error "Failed to set ownership of OpenVPN configuration directory" + chmod -R 777 /etc/openvpn || handle_error "Failed to set permissions of OpenVPN configuration directory" + chmod -R 777 /etc/openvpn/easy-rsa || handle_error "Failed to set permissions of OpenVPN easy-rsa directory" + ;; + *) + handle_error "Unsupported Ubuntu version for OpenVPN installation" + ;; +esac +COMPLETED_STEPS+=("OpenVPN installed") + +# Configure Systemd sandbox overrides for OpenVPN writes +log_step "Configuring Systemd sandbox overrides for OpenVPN writes" +mkdir -p /etc/systemd/system/php${PHP_VERSION}-fpm.service.d || handle_error "Failed to create PHP systemd override directory" +cat > /etc/systemd/system/php${PHP_VERSION}-fpm.service.d/override.conf << 'EOF' +[Service] +ReadWritePaths=/etc/openvpn +EOF + +mkdir -p /etc/systemd/system/supervisor.service.d || handle_error "Failed to create Supervisor systemd override directory" +cat > /etc/systemd/system/supervisor.service.d/override.conf << 'EOF' +[Service] +ReadWritePaths=/etc/openvpn +EOF +COMPLETED_STEPS+=("Systemd sandbox overrides configured") + +# Install Laravel cron (Safe Append Fix) +log_step "Installing cron" +(crontab -l 2>/dev/null; echo "* * * * * php /var/www/html/artisan schedule:run >> /dev/null 2>&1") | crontab - || handle_error "Failed to install Laravel cron job" +COMPLETED_STEPS+=("Cron job installed") + +# Update sudoers for www-data user (Updated openvpn explicit target) +log_step "Updating sudoers for www-data user" +cat >> /etc/sudoers << 'EOL' +www-data ALL=NOPASSWD: /bin/systemctl start openvpn@server +www-data ALL=NOPASSWD: /bin/systemctl stop openvpn@server +www-data ALL=NOPASSWD: /bin/systemctl restart openvpn@server +www-data ALL=NOPASSWD: /bin/systemctl status openvpn@server +www-data ALL=NOPASSWD: /bin/systemctl reload openvpn@server +www-data ALL=NOPASSWD: /bin/systemctl enable openvpn@server +www-data ALL=NOPASSWD: /bin/systemctl disable openvpn@server +www-data ALL=NOPASSWD: /bin/systemctl start freeradius +www-data ALL=NOPASSWD: /bin/systemctl stop freeradius +www-data ALL=NOPASSWD: /bin/systemctl restart freeradius +www-data ALL=NOPASSWD: /bin/systemctl status freeradius +www-data ALL=NOPASSWD: /bin/systemctl reload freeradius +www-data ALL=NOPASSWD: /bin/systemctl enable freeradius +www-data ALL=NOPASSWD: /bin/systemctl disable freeradius +www-data ALL=NOPASSWD: /bin/supervisorctl stop all +www-data ALL=NOPASSWD: /bin/supervisorctl reread +www-data ALL=NOPASSWD: /bin/supervisorctl update +www-data ALL=NOPASSWD: /bin/supervisorctl start all +www-data ALL=NOPASSWD: /bin/supervisorctl restart all +www-data ALL=NOPASSWD: /bin/supervisorctl status +www-data ALL=NOPASSWD: /bin/systemctl restart supervisor +www-data ALL=NOPASSWD: /bin/systemctl status ssh +www-data ALL=NOPASSWD: /var/www/html/sh/set_permissions.sh +www-data ALL=NOPASSWD: /var/www/html/sh/restart-services.sh +EOL +COMPLETED_STEPS+=("Sudoers updated for www-data user") + +# Open Firewall Ports and enable ufw +log_step "Opening firewall ports and enabling ufw" +ufw allow ssh || handle_error "Failed to allow SSH through firewall" +ufw allow 9080/tcp || handle_error "Failed to allow port 9080 through firewall" +ufw allow http || handle_error "Failed to allow HTTP through firewall" +ufw allow https || handle_error "Failed to allow HTTPS through firewall" +ufw allow 1194/tcp || handle_error "Failed to allow OpenVPN through firewall" +ufw allow 1812:1813/udp || handle_error "Failed to allow FreeRADIUS through firewall" +ufw reload || handle_error "Failed to reload firewall rules" +yes | ufw enable || handle_error "Failed to enable firewall" +COMPLETED_STEPS+=("Firewall ports opened and ufw enabled") + +# Test FreeRADIUS configuration +log_step "Checking FreeRADIUS files" + +if [ ! -f "/etc/freeradius/radiusd.conf" ]; then + log_info "FreeRADIUS configuration files missing, reinstalling and reconfiguring FreeRADIUS package" + apt-get purge -y freeradius freeradius-common freeradius-config 2>/dev/null || echo "FreeRADIUS not installed" + apt-get autoremove -y 2>/dev/null + apt-get install -y freeradius freeradius-mysql freeradius-config || handle_error "Failed to reinstall FreeRADIUS" + dpkg-reconfigure -f noninteractive freeradius-config 2>/dev/null || echo "Reconfigure not needed" +fi + +if [ ! -f "/etc/freeradius/radiusd.conf" ]; then + log_info "Creating minimal radiusd.conf configuration" + mkdir -p /etc/freeradius || handle_error "Failed to create freeradius config dir" + cat > /etc/freeradius/radiusd.conf << 'EOF' +prefix = /usr +exec_prefix = ${prefix} +sysconfdir = /etc +localstatedir = /var +sbindir = ${exec_prefix}/sbin +logdir = /var/log/freeradius +raddbdir = /etc/freeradius +radacctdir = ${logdir}/radacct + +name = freeradius +confdir = ${raddbdir} +modconfdir = ${confdir}/mods-config +certdir = ${confdir}/certs +cadir = ${confdir}/certs +run_dir = ${localstatedir}/run/${name} + +db_dir = ${raddbdir} +libdir = /usr/lib/freeradius +pidfile = ${run_dir}/${name}.pid + +correct_escapes = true +max_request_time = 30 +cleanup_delay = 5 +max_requests = 16384 +hostname_lookups = no + +log { +destination = files +colourise = yes +file = ${logdir}/radius.log +syslog_facility = daemon +stripped_names = no +auth = no +auth_badpass = no +auth_goodpass = no +msg_denied = "You are already logged in - access denied" +} + +checkrad = ${sbindir}/checkrad + +security { +allow_core_dumps = no +max_attributes = 200 +reject_delay = 1 +status_server = yes +} + +proxy_requests = yes +$INCLUDE proxy.conf +$INCLUDE clients.conf + +thread pool { +start_servers = 5 +max_servers = 32 +min_spare_servers = 3 +max_spare_servers = 10 +max_requests_per_server = 0 +auto_limit_acct = no +} + +$INCLUDE sites-enabled/ +$INCLUDE mods-enabled/ + +policy { +$INCLUDE policy.d/ +} + +instantiate { +} +EOF + chmod 644 /etc/freeradius/radiusd.conf || handle_error "Failed to set radiusd.conf permissions" + log_success "Created minimal radiusd.conf configuration" +fi + +if [ -f "/etc/freeradius/mods-available/sql" ]; then + ln -sf /etc/freeradius/mods-available/sql /etc/freeradius/mods-enabled/ || handle_error "Failed to re-enable SQL module" +fi +COMPLETED_STEPS+=("Completed checking FreeRADIUS files") + +# Enable buffered-sql site +log_step "Enabling FreeRADIUS buffered-sql site" +mkdir -p /etc/freeradius/sites-enabled || handle_error "Failed to create FreeRADIUS sites-enabled directory" +ln -sf /etc/freeradius/sites-available/buffered-sql /etc/freeradius/sites-enabled/buffered-sql || handle_error "Failed to enable buffered-sql site" +COMPLETED_STEPS+=("FreeRADIUS buffered-sql site enabled") + +# Enable SQL module for FreeRADIUS +log_step "Enabling SQL module" +mkdir -p /etc/freeradius/mods-enabled || handle_error "Failed to create FreeRADIUS mods-enabled directory" +ln -sf /etc/freeradius/mods-available/sql /etc/freeradius/mods-enabled/sql || handle_error "Failed to enable SQL module" +COMPLETED_STEPS+=("FreeRADIUS SQL module enabled") + +# Write new FreeRADIUS SQL module +log_step "Writing new FreeRADIUS SQL module" +SQL_FILE="/etc/freeradius/mods-available/sql" +cat > "$SQL_FILE" < /tmp/new_accounting_block +accounting { +sql +exec +attr_filter.accounting_response +} +EOF + awk ' + BEGIN { skip = 0 } + /^accounting[ \t]*{/ { print_block = 1; print_file("/tmp/new_accounting_block"); skip = 1; next } + /^[ \t]*}/ { if (skip) { skip = 0; next } } + !skip { print } + function print_file(file) { + while ((getline line < file) > 0) print line; + close(file) + } + ' "$DEFAULT_SITE" > /tmp/tmp_site && mv /tmp/tmp_site "$DEFAULT_SITE" +else + handle_error "Default site configuration file not found" +fi +COMPLETED_STEPS+=("FreeRADIUS default site configured") + +# Apply Systemd Sandbox changes from the overrides +systemctl daemon-reload || handle_error "Failed to reload systemd daemon" + +# Enable and start all services (Consolidated Block) +log_step "Enabling and restarting all services" + +SERVICES=(nginx php${PHP_VERSION}-fpm supervisor openvpn@server freeradius valkey-server) +for service in "${SERVICES[@]}"; do + systemctl enable "$service" || handle_error "Failed to enable $service" + systemctl restart "$service" || handle_error "Failed to restart $service" +done +COMPLETED_STEPS+=("All services enabled and restarted") + +# Create a script to update memory configurations +cat > /usr/local/bin/update_memory_config.sh << 'EOL' +#!/usr/bin/env bash +# Optimized Co-hosted Tuning Script for SimpleISP +# Balances MariaDB, Valkey, PHP-FPM, and FreeRADIUS. + +set -euo pipefail + +LOG_FILE="/var/log/memory_config_updates.log" +log() { echo "[$(date +'%F %T')] $1" >> "$LOG_FILE"; } + +log "Starting memory configuration update" + +# 1. Introspect System Resources +TOTAL_RAM_MB=$(free -m | awk '/^Mem:/{print $2}') +VCPUS=$(nproc --all) + +log "Detected RAM: ${TOTAL_RAM_MB}MB | CPU Cores: ${VCPUS}" + +# 2. Budget Memory +# OS Reserve: 15% or 1024MB, whichever is higher +OS_RESERVE_MB=$(( TOTAL_RAM_MB * 15 / 100 )) +[ "$OS_RESERVE_MB" -lt 1024 ] && OS_RESERVE_MB=1024 + +# MariaDB: 40% of total RAM +MARIADB_POOL_MB=$(( TOTAL_RAM_MB * 40 / 100 )) +[ "$MARIADB_POOL_MB" -lt 256 ] && MARIADB_POOL_MB=256 + +# Valkey/Redis: 15% of total RAM +VALKEY_MB=$(( TOTAL_RAM_MB * 15 / 100 )) +[ "$VALKEY_MB" -lt 128 ] && VALKEY_MB=128 + +# PHP-FPM: The remaining RAM +REMAINING_MB=$(( TOTAL_RAM_MB - OS_RESERVE_MB - MARIADB_POOL_MB - VALKEY_MB )) +[ "$REMAINING_MB" -lt 256 ] && REMAINING_MB=256 + +log "Budget -> OS: ${OS_RESERVE_MB}MB | DB: ${MARIADB_POOL_MB}MB | Cache: ${VALKEY_MB}MB | PHP: ${REMAINING_MB}MB" + +# 3. Apply MariaDB Tuning +MARIADB_CONF="/etc/mysql/mariadb.conf.d/99-autotune.cnf" +log "Tuning MariaDB..." +cat > "$MARIADB_CONF" < 1024 ? MARIADB_POOL_MB / 1024 : 1 )) +innodb_log_file_size = $(( MARIADB_POOL_MB / 4 ))M +max_connections = $(( VCPUS * 150 )) +tmp_table_size = 64M +max_heap_table_size = 64M +EOF +systemctl restart mariadb || service mysql restart + +# 4. Apply Valkey Tuning +VALKEY_CONF="/etc/valkey/valkey.conf" +if [ -f "$VALKEY_CONF" ]; then + log "Tuning Valkey..." + # Replace existing maxmemory line or append it + sed -i -E "s/^#?maxmemory .*/maxmemory ${VALKEY_MB}mb/" "$VALKEY_CONF" + # Ensure eviction policy is set safely for Laravel (protects queues/sessions without expirations) + if ! grep -q "^maxmemory-policy" "$VALKEY_CONF"; then + echo "maxmemory-policy volatile-lru" >> "$VALKEY_CONF" + fi + systemctl restart valkey-server || true +fi + +# 5. Apply PHP-FPM Tuning +# Assuming Laravel average process size = 80MB +AVG_PHP_RSS_MB=80 +MAX_CHILDREN=$(( REMAINING_MB / AVG_PHP_RSS_MB )) +[ "$MAX_CHILDREN" -lt 5 ] && MAX_CHILDREN=5 + +# Find active PHP-FPM pool file +POOL_CONF=$(find /etc/php/*/fpm/pool.d/ -name "www.conf" | head -n 1) +if [ -n "$POOL_CONF" ]; then + log "Tuning PHP-FPM in $POOL_CONF..." + sed -i -E "s/^[;#]?pm.max_children =.*/pm.max_children = ${MAX_CHILDREN}/" "$POOL_CONF" + sed -i -E "s/^[;#]?pm.start_servers =.*/pm.start_servers = $(( MAX_CHILDREN * 20 / 100 + 1 ))/" "$POOL_CONF" + sed -i -E "s/^[;#]?pm.min_spare_servers =.*/pm.min_spare_servers = $(( MAX_CHILDREN * 10 / 100 + 1 ))/" "$POOL_CONF" + sed -i -E "s/^[;#]?pm.max_spare_servers =.*/pm.max_spare_servers = $(( MAX_CHILDREN * 30 / 100 + 1 ))/" "$POOL_CONF" + + # Reload PHP-FPM + PHP_SVC=$(basename $(dirname $(dirname "$POOL_CONF")) | sed 's|^|php|;s|$|-fpm|') + systemctl reload "$PHP_SVC" || systemctl restart "$PHP_SVC" +fi + +# 6. Apply FreeRADIUS Tuning +RADIUS_CONF=$(find /etc/freeradius -name "radiusd.conf" 2>/dev/null | head -n 1) +if [ -n "$RADIUS_CONF" ]; then + log "Tuning FreeRADIUS in $RADIUS_CONF..." + # Start 2 threads per core, max out at 6 per core + START_SERVERS=$(( VCPUS * 2 )) + MAX_SERVERS=$(( VCPUS * 6 )) + + awk -v s="$START_SERVERS" -v m="$MAX_SERVERS" ' + BEGIN { inpool=0 } + /^thread pool \{/ { inpool=1; print; next } + /^\}/ { inpool=0; print; next } + inpool && /^[[:space:]]*start_servers/ { printf "\tstart_servers = %s\n", s; next } + inpool && /^[[:space:]]*max_servers/ { printf "\tmax_servers = %s\n", m; next } + { print } + ' "$RADIUS_CONF" > "${RADIUS_CONF}.tmp" && mv "${RADIUS_CONF}.tmp" "$RADIUS_CONF" + + systemctl restart freeradius || true +fi + +log "Memory configuration update completed" +EOL + +# Make the script executable +chmod +x /usr/local/bin/update_memory_config.sh || handle_error "Failed to make memory config script executable" + +# Add cron job to run at 3 AM daily (Safe Append Fix applied) +(crontab -l 2>/dev/null; echo "0 3 * * * /usr/local/bin/update_memory_config.sh") | crontab - || handle_error "Failed to add cron job" + +# Run the script once to apply initial configuration +/usr/local/bin/update_memory_config.sh || handle_error "Failed to run initial memory configuration" +COMPLETED_STEPS+=("Automatic memory configuration script installed at /usr/local/bin/update_memory_config.sh") + +# Final verification +log_step "Verifying all services are running" +for service in nginx mariadb freeradius valkey-server php${PHP_VERSION}-fpm; do + if ! systemctl is-active --quiet $service; then + log_warning "$service is not running" + systemctl status $service + else + log_success "$service is running" + fi +done + +# Complete installation message +log_success "Installation completed successfully!" +echo "You can find your database credentials in $DB_CREDENTIALS_FILE" +echo "Your SimpleISP installation is available at: https://$DOMAIN" diff --git a/simplespot.sh b/simplespot.sh new file mode 100644 index 0000000..65f1267 --- /dev/null +++ b/simplespot.sh @@ -0,0 +1,1073 @@ +#!/bin/bash + +# Setup logging and error handling +INSTALL_LOG="/root/install.txt" +STEP_COUNT=0 +COMPLETED_STEPS=() + +# Get server hostname and set email +DOMAIN=$(hostname -f) +EMAIL_ADDRESS="simpluxsolutions@gmail.com" + +# Configuration variables - ONLY DIFFERENCES BETWEEN SCRIPTS +GITHUB_REPO_URL="https://github.com/simpleisp/simplespot.git" +PHP_VERSION="8.2" + +# Logging functions +log_info() { echo "ℹ️ INFO: $1" | tee -a "$INSTALL_LOG"; } +log_success() { echo "✅ SUCCESS: $1" | tee -a "$INSTALL_LOG"; } +log_error() { echo "❌ ERROR: $1" | tee -a "$INSTALL_LOG"; } +log_step() { STEP_COUNT=$((STEP_COUNT + 1)); echo "👉 STEP $STEP_COUNT: $1" | tee -a "$INSTALL_LOG"; } + +handle_error() { + log_error "$1" + echo -e "\nCompleted steps before failure:" + printf '%s\n' "${COMPLETED_STEPS[@]}" + echo -e "\nCheck $INSTALL_LOG for more details" + exit 1 +} + +# Initialize log file +touch "$INSTALL_LOG" || { echo "Cannot create log file"; exit 1; } +echo "SimpleISP Installation Log - $(date '+%Y-%m-%d %H:%M:%S')" > "$INSTALL_LOG" +echo "----------------------------------------" >> "$INSTALL_LOG" + +# Configure system for Valkey (memory overcommit and other optimizations) +log_step "Configuring system for Valkey" + +# Enable memory overcommit +if ! grep -q "^vm.overcommit_memory" /etc/sysctl.conf; then + echo "vm.overcommit_memory = 1" | tee -a /etc/sysctl.conf + sysctl -p /etc/sysctl.conf + log_info "Enabled memory overcommit in sysctl" +else + log_info "Memory overcommit already configured in sysctl" +fi +COMPLETED_STEPS+=("System configured for Valkey") + +# Check for cleanup marker file +CLEANUP_MARKER="/root/.simpleisp_cleanup_done" +REINSTALL=false + +if [ -f "$CLEANUP_MARKER" ]; then + log_info "Detected previous cleanup ($(cat $CLEANUP_MARKER))" + log_info "Forcing reinstallation of critical directories and files" + REINSTALL=true + + # Remove the marker file after handling it + rm -f "$CLEANUP_MARKER" + log_success "Cleanup marker processed and removed" +fi + +# Ensure script runs as root +log_step "Checking root privileges" +if [ "$EUID" -ne 0 ]; then handle_error "Please run as root"; fi +COMPLETED_STEPS+=("Root check passed") + +# Get Ubuntu version +log_step "Detecting Ubuntu version" +UBUNTU_VERSION=$(lsb_release -cs) || handle_error "Failed to detect Ubuntu version" +log_info "Detected Ubuntu version: $UBUNTU_VERSION" +COMPLETED_STEPS+=("Ubuntu version detected: $UBUNTU_VERSION") + +# Set PHP Repo for Ubuntu 24.04 (Noble) +log_step "Adding PHP repository" +if [ "$UBUNTU_VERSION" = "noble" ]; then + # Set up PHP repository for Ubuntu 24.04 + gpgKey='B8DC7E53946656EFBCE4C1DD71DAEAAB4AD4CAB6' + gpgKeyPath='/etc/apt/keyrings/ondrej-ubuntu-php.gpg' + gpgURL="https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x${gpgKey}" + + # Create keyrings directory if it doesn't exist + install -d -m 0755 /etc/apt/keyrings || handle_error "Failed to create keyrings directory" + + # Download and set up GPG key + curl "${gpgURL}" | gpg --dearmor | tee ${gpgKeyPath} >/dev/null || handle_error "Failed to setup PHP GPG key" + gpg --dry-run --quiet --import --import-options import-show ${gpgKeyPath} + + # Create the sources file for PHP repository + cat > /etc/apt/sources.list.d/ondrej-ubuntu-php-noble.sources << 'EOL' +Types: deb +URIs: https://ppa.launchpadcontent.net/ondrej/php/ubuntu/ +Suites: noble +Components: main +Signed-By: /etc/apt/keyrings/ondrej-ubuntu-php.gpg +EOL +else + # For other Ubuntu versions, use the traditional PPA method + LC_ALL=C.UTF-8 add-apt-repository ppa:ondrej/php -y || handle_error "Failed to add PHP repository" +fi +COMPLETED_STEPS+=("PHP repository added") + +# Set NetworkRADIUS PGP public key +log_step "Configuring NetworkRADIUS repository" +install -d -o root -g root -m 0755 /etc/apt/keyrings || handle_error "Failed to create keyrings directory" +curl -s 'https://packages.networkradius.com/pgp/packages%40networkradius.com' | sudo tee /etc/apt/keyrings/packages.networkradius.com.asc > /dev/null || handle_error "Failed to download NetworkRADIUS PGP key" +COMPLETED_STEPS+=("NetworkRADIUS PGP key installed") + +# Add NetworkRADIUS APT preferences +printf 'Package: /freeradius/\nPin: origin "packages.networkradius.com"\nPin-Priority: 999\n' | sudo tee /etc/apt/preferences.d/networkradius > /dev/null || handle_error "Failed to set NetworkRADIUS preferences" + +# Add NetworkRADIUS repository based on Ubuntu version +case $UBUNTU_VERSION in + "noble") REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/noble noble main" ;; + "jammy") REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/jammy jammy main" ;; + "focal") REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/focal focal main" ;; + *) handle_error "Unsupported Ubuntu version: $UBUNTU_VERSION" ;; +esac + +echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/packages.networkradius.com.asc] $REPO_URL" | sudo tee /etc/apt/sources.list.d/networkradius.list > /dev/null || handle_error "Failed to add NetworkRADIUS repository" +log_success "NetworkRADIUS repository configured for Ubuntu $UBUNTU_VERSION" +COMPLETED_STEPS+=("NetworkRADIUS repository configured") + +# Set environment variable to avoid interactive prompts +export DEBIAN_FRONTEND=noninteractive + +# Update and upgrade system +log_step "Updating system packages" +apt-get update || handle_error "Failed to update package lists" +apt-get upgrade -y || handle_error "Failed to upgrade packages" +COMPLETED_STEPS+=("System packages updated") + +# Install required packages (Cleaned virtual PHP packages and freeradius-rest) +log_step "Installing required packages" +INSTALL_PACKAGES="nginx-full python3-certbot-nginx php${PHP_VERSION}-fpm php${PHP_VERSION}-mysql php${PHP_VERSION}-cli php${PHP_VERSION}-curl php${PHP_VERSION}-zip php${PHP_VERSION}-common php${PHP_VERSION}-gd php${PHP_VERSION}-mbstring php${PHP_VERSION}-xml php${PHP_VERSION}-dev php${PHP_VERSION}-bcmath php${PHP_VERSION}-intl php${PHP_VERSION}-redis git unzip curl wget software-properties-common apt-transport-https ca-certificates gnupg lsb-release supervisor valkey-server valkey-tools valkey-redis-compat valkey-sentinel ufw openvpn easy-rsa freeradius freeradius-mysql freeradius-utils mariadb-server mariadb-client" + +if [ "$REINSTALL" = true ]; then + log_info "Reinstalling packages (forcing configuration file replacement)" + apt-get install --reinstall -y -o Dpkg::Options::="--force-confmiss" $INSTALL_PACKAGES || handle_error "Failed to reinstall packages" +else + apt-get install -y $INSTALL_PACKAGES || handle_error "Failed to install packages" +fi +COMPLETED_STEPS+=("Required packages installed") + +# Configure Valkey service overrides +log_step "Configuring Valkey service overrides" +VKEY_OVERRIDE_DIR="/etc/systemd/system/valkey.service.d" +VKEY_OVERRIDE_FILE="${VKEY_OVERRIDE_DIR}/override.conf" + +mkdir -p "$VKEY_OVERRIDE_DIR" +cat > "$VKEY_OVERRIDE_FILE" << 'EOF' +[Unit] + +[Service] +# Increase timeouts to prevent premature termination +TimeoutStartSec=300 +TimeoutStopSec=300 + +# Ensure service restarts on failure +Restart=always +RestartSec=10s + +# Disable OOM killer for Valkey +OOMScoreAdjust=-1000 +EOF + +# Apply changes and restart Valkey +log_info "Applying Valkey service configuration..." + +# Configure Valkey with optimized settings for FreeRADIUS +log_step "Configuring Valkey with optimized settings" + +# Calculate optimal memory allocation (Safe 15% of available RAM, capped at 3GB, minimum 1GB) +TOTAL_RAM_KB=$(grep MemTotal /proc/meminfo | awk '{print $2}') +TOTAL_RAM_MB=$((TOTAL_RAM_KB / 1024)) +MAX_MEMORY_MB=$((TOTAL_RAM_MB * 15 / 100)) +if [ "$MAX_MEMORY_MB" -gt 3072 ]; then MAX_MEMORY_MB=3072; fi +if [ "$MAX_MEMORY_MB" -lt 1024 ]; then MAX_MEMORY_MB=1024; fi + +log_info "Configuring Valkey with ${MAX_MEMORY_MB}MB memory allocation" + +# Create Valkey configuration directory if it doesn't exist +mkdir -p /etc/valkey || handle_error "Failed to create Valkey configuration directory" + +# Configure Valkey with optimized settings for FreeRADIUS +cat > /etc/valkey/valkey.conf << EOL +# Valkey configuration for FreeRADIUS +bind 0.0.0.0 ::0 +protected-mode yes +port 6379 +tcp-backlog 511 +timeout 0 +tcp-keepalive 300 +daemonize no +supervised systemd +pidfile /var/run/valkey/valkey.pid +loglevel notice +logfile /var/log/valkey/valkey.log +databases 16 + +# Memory management +maxmemory ${MAX_MEMORY_MB}mb +maxmemory-policy volatile-lru +maxmemory-samples 5 + +# AOF persistence (enabled for better durability) +appendonly yes +dir /var/lib/valkey +appendfilename "appendonly.aof" +appendfsync everysec +no-appendfsync-on-rewrite no +auto-aof-rewrite-percentage 100 +auto-aof-rewrite-min-size 64mb +aof-load-truncated yes +aof-rewrite-incremental-fsync yes + +# Performance optimizations +stop-writes-on-bgsave-error no +rdbcompression yes +rdbchecksum yes +dbfilename dump.rdb + +# Disable RDB snapshots since we're using AOF +save "" + +# Security (Reuse the same password as MySQL for simplicity) +requirepass "$MYSQL_PASSWORD" + +# Network +tcp-keepalive 300 +repl-timeout 60 +repl-ping-slave-period 10 +repl-backlog-size 1mb +repl-backlog-ttl 3600 +timeout 0 +tcp-keepalive 300 + +# Disable dangerous commands +rename-command FLUSHDB "" +rename-command FLUSHALL "" +rename-command CONFIG "" +rename-command SHUTDOWN "" + +# Tune threads and clients +io-threads 2 +io-threads-do-reads yes +maxclients 10000 + +# Tune data structures +active-expire-effort 1 +client-output-buffer-limit normal 0 0 0 +client-output-buffer-limit replica 256mb 64mb 60 +client-output-buffer-limit pubsub 32mb 8mb 60 +hash-max-ziplist-entries 512 +hash-max-ziplist-value 64 +list-max-ziplist-size -2 +set-max-intset-entries 512 +zset-max-ziplist-entries 128 +zset-max-ziplist-value 64 +hll-sparse-max-bytes 3000 +stream-node-max-bytes 4096 +stream-node-max-entries 100 + +# Enable active defragmentation +active-defrag-threshold-lower 10 +active-defrag-threshold-upper 100 +active-defrag-ignore-bytes 100mb +active-defrag-cycle-min 5 +active-defrag-cycle-max 75 +active-defrag-max-scan-fields 1000 +EOL + +# Restart Valkey to apply new configuration +systemctl daemon-reexec || log_warning "daemon-reexec failed (non-critical)" +systemctl daemon-reload || handle_error "Failed to reload systemd daemon" +systemctl restart valkey-server || handle_error "Failed to restart Valkey" +systemctl enable valkey-server || handle_error "Failed to enable Valkey" + +# Verify Valkey is running +log_step "Verifying Valkey service status" +if systemctl is-active --quiet valkey-server; then + log_success "Valkey service is running" + COMPLETED_STEPS+=("Valkey configured with ${MAX_MEMORY_MB}MB memory allocation") +else + log_warning "Valkey service is not running as expected. Checking status..." + systemctl status valkey-server --no-pager || true + + log_info "Attempting to start Valkey service..." + if systemctl start valkey-server; then + log_success "Successfully started Valkey service" + COMPLETED_STEPS+=("Valkey configured with ${MAX_MEMORY_MB}MB memory allocation") + else + log_error "Failed to start Valkey service. Please check the logs with: journalctl -u valkey-server -n 50" + log_warning "Continuing installation despite Valkey service issue..." + COMPLETED_STEPS+=("Valkey configuration completed but service failed to start") + fi +fi + +# Create Valkey debug script +log_step "Creating Valkey debug script" +cat > /usr/local/bin/valkey-debug.sh << 'EOF' +#!/bin/bash + +VALKEY_HOST="127.0.0.1" +VALKEY_PORT="6379" + +echo "=== Valkey Status ===" +systemctl status valkey --no-pager -l + +echo -e "\n=== Valkey Key Statistics ===" +echo "Total Keys in DB 0: $(valkey-cli -h $VALKEY_HOST -p $VALKEY_PORT dbsize)" +EOF + +chmod +x /usr/local/bin/valkey-debug.sh || handle_error "Failed to make Valkey debug script executable" +COMPLETED_STEPS+=("Valkey monitoring configured") + +# Add monitoring cron job (Safe Append Fix) +log_step "Adding monitoring cron job" +(crontab -l 2>/dev/null; echo "*/5 * * * * /usr/local/bin/valkey-debug.sh") | crontab - || handle_error "Failed to install monitoring cron job" +COMPLETED_STEPS+=("Monitoring cron job added") + +# Verify Valkey is working +log_step "Verifying Valkey installation" +if ! systemctl is-active --quiet valkey; then handle_error "Valkey service is not running"; fi + +# Test Valkey connectivity and basic operations +if [ "$(valkey-cli ping)" != "PONG" ]; then handle_error "Valkey is not responding to ping"; fi +if [ "$(valkey-cli set test_key test_value)" != "OK" ]; then handle_error "Valkey write operation failed"; fi +if [ "$(valkey-cli get test_key)" != "test_value" ]; then handle_error "Valkey read operation failed"; fi +if [ "$(valkey-cli del test_key)" != "1" ]; then handle_error "Valkey delete operation failed"; fi +if ! valkey-cli info | grep -q "valkey_version"; then handle_error "Unable to get Valkey server information"; fi +COMPLETED_STEPS+=("Valkey functionality verified") + +# Set Default PHP Version +log_step "Setting default PHP version" +update-alternatives --set php /usr/bin/php${PHP_VERSION} || handle_error "Failed to set default PHP version" +COMPLETED_STEPS+=("PHP ${PHP_VERSION} set as default") + +# Install and configure ionCube Loader +log_step "Installing ionCube Loader" +if [ -d "/usr/local/ioncube" ] && [ -f "/usr/local/ioncube/ioncube_loader_lin_${PHP_VERSION}.so" ]; then + log_info "ionCube Loader already exists, skipping download and installation" + COMPLETED_STEPS+=("ionCube Loader reused (already exists)") +else + log_info "ionCube Loader not found, downloading and installing" + cd /tmp || handle_error "Failed to change to /tmp directory" + wget -O ioncube.zip "https://downloads.ioncube.com/loader_downloads/ioncube_loaders_lin_x86-64.zip" || handle_error "Failed to download ionCube" + unzip -q ioncube.zip || handle_error "Failed to extract ionCube" + rm -rf /usr/local/ioncube 2>/dev/null + mv ioncube /usr/local/ || handle_error "Failed to move ionCube to /usr/local" + COMPLETED_STEPS+=("ionCube Loader downloaded and installed") +fi + +# Create ionCube ini file with absolute path +cat > /etc/php/${PHP_VERSION}/mods-available/ioncube.ini << EOL +zend_extension = /usr/local/ioncube/ioncube_loader_lin_${PHP_VERSION}.so +EOL + +# Enable ionCube for PHP CLI and FPM +ln -sf /etc/php/${PHP_VERSION}/mods-available/ioncube.ini /etc/php/${PHP_VERSION}/cli/conf.d/00-ioncube.ini || handle_error "Failed to enable ionCube for CLI" +ln -sf /etc/php/${PHP_VERSION}/mods-available/ioncube.ini /etc/php/${PHP_VERSION}/fpm/conf.d/00-ioncube.ini || handle_error "Failed to enable ionCube for FPM" +systemctl restart php${PHP_VERSION}-fpm || handle_error "Failed to restart PHP-FPM" + +# Verify ionCube installation +if php -v | grep -q "ionCube PHP Loader"; then + log_success "ionCube Loader installed and enabled successfully" + COMPLETED_STEPS+=("ionCube Loader installed and configured") +else + handle_error "ionCube Loader installation verification failed" +fi + +# Start and enable MariaDB +log_step "Configuring MariaDB" +if [ ! -d "/var/lib/mysql/mysql" ]; then + log_info "Initializing MariaDB system database" + mysql_install_db --user=mysql --datadir=/var/lib/mysql || handle_error "Failed to initialize MariaDB" +fi + +systemctl start mariadb || handle_error "Failed to start MariaDB" +systemctl enable mariadb || handle_error "Failed to enable MariaDB" + +# Ensure debian-start script exists (recreate if missing) +if [ ! -f "/etc/mysql/debian-start" ]; then + log_info "Creating missing /etc/mysql/debian-start script" + cat > /etc/mysql/debian-start << 'EOF' +#!/bin/bash +if [ "$(id -u)" != "0" ]; then echo "This script must be run as root" 1>&2; exit 1; fi +if ! /usr/bin/mysqladmin --defaults-file=/etc/mysql/debian.cnf ping > /dev/null 2>&1; then exit 0; fi +exit 0 +EOF + chmod +x /etc/mysql/debian-start || handle_error "Failed to make debian-start executable" + log_success "Created /etc/mysql/debian-start script" +fi +COMPLETED_STEPS+=("MariaDB initialized, started and enabled") + +# Configure MySQL to allow remote connections +log_step "Configuring MySQL for remote connections" +mkdir -p /etc/mysql/mariadb.conf.d/ || handle_error "Failed to create MariaDB conf directory" + +cat > /etc/mysql/mariadb.conf.d/50-server.cnf << 'EOL' +[server] +[mariadbd] +user = mysql +pid-file = /run/mysqld/mysqld.pid +basedir = /usr +datadir = /var/lib/mysql +tmpdir = /tmp +skip-external-locking +bind-address = 0.0.0.0 +key_buffer_size = 16M +max_allowed_packet = 16M +thread_stack = 192K +thread_cache_size = 8 +myisam-recover-options = BACKUP +query_cache_limit = 1M +query_cache_size = 16M +expire_logs_days = 10 +max_binlog_size = 100M +character-set-server = utf8mb4 +collation-server = utf8mb4_general_ci +innodb_buffer_pool_size = 1G +innodb_log_file_size = 256M +innodb_flush_method = O_DIRECT +innodb_flush_log_at_trx_commit = 2 +innodb_file_per_table = 1 +[embedded] +[mariadb] +EOL + +systemctl restart mariadb || handle_error "Failed to restart MariaDB after configuration change" +COMPLETED_STEPS+=("MySQL configured for remote connections") + +# Generate random credentials or reuse existing ones +DB_CREDENTIALS_FILE="/root/db.txt" +if [ -f "$DB_CREDENTIALS_FILE" ] && [ -r "$DB_CREDENTIALS_FILE" ]; then + log_step "Found existing database credentials, reusing them" + MYSQL_USER=$(grep "^DB_USERNAME=" "$DB_CREDENTIALS_FILE" | cut -d'=' -f2) + MYSQL_PASSWORD=$(grep "^DB_PASSWORD=" "$DB_CREDENTIALS_FILE" | cut -d'=' -f2) + MYSQL_DATABASE=$(grep "^DB_DATABASE=" "$DB_CREDENTIALS_FILE" | cut -d'=' -f2) + + if [ -n "$MYSQL_USER" ] && [ -n "$MYSQL_PASSWORD" ] && [ -n "$MYSQL_DATABASE" ]; then + log_info "Reusing existing database credentials: User=$MYSQL_USER, Database=$MYSQL_DATABASE" + COMPLETED_STEPS+=("Database credentials reused from existing file") + else + log_info "Existing db.txt file is incomplete, generating new credentials" + MYSQL_USER="user_$(openssl rand -hex 3)" + MYSQL_PASSWORD="$(openssl rand -base64 12)" + MYSQL_DATABASE="radius" + COMPLETED_STEPS+=("New database credentials generated (existing file was incomplete)") + fi +else + log_step "No existing database credentials found, generating new ones" + MYSQL_USER="user_$(openssl rand -hex 3)" + MYSQL_PASSWORD="$(openssl rand -base64 12)" + MYSQL_DATABASE="radius" + COMPLETED_STEPS+=("New database credentials generated") +fi + +# Save database credentials +log_step "Saving database credentials" +echo "MySQL Credentials:" > "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" +echo "DB_HOST=localhost" >> "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" +echo "DB_PORT=3306" >> "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" +echo "DB_DATABASE=$MYSQL_DATABASE" >> "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" +echo "DB_USERNAME=$MYSQL_USER" >> "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" +echo "DB_PASSWORD=$MYSQL_PASSWORD" >> "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" +COMPLETED_STEPS+=("Database credentials saved") + +# Secure MariaDB installation +log_step "Securing MariaDB installation" +mysql -e "DELETE FROM mysql.user WHERE User='';" || handle_error "Failed to delete anonymous MariaDB user" +mysql -e "DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');" || handle_error "Failed to delete remote MariaDB root user" +mysql -e "DROP DATABASE IF EXISTS test;" || handle_error "Failed to delete test database" +mysql -e "DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%';" || handle_error "Failed to delete test database" +mysql -e "FLUSH PRIVILEGES;" || handle_error "Failed to flush MariaDB privileges" +COMPLETED_STEPS+=("MariaDB installation secured") + +# Create database and user +log_step "Creating database and user" +mysql -e "CREATE DATABASE $MYSQL_DATABASE;" || handle_error "Failed to create database" +mysql -e "CREATE USER '$MYSQL_USER'@'%' IDENTIFIED BY '$MYSQL_PASSWORD';" || handle_error "Failed to create database user" +mysql -e "GRANT ALL PRIVILEGES ON $MYSQL_DATABASE.* TO '$MYSQL_USER'@'%';" || handle_error "Failed to grant database privileges" +mysql -e "FLUSH PRIVILEGES;" || handle_error "Failed to flush MariaDB privileges" +COMPLETED_STEPS+=("Database and user created with full access") + +# Install Composer +log_step "Installing Composer" +if ! command -v composer &> /dev/null; then + php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" || handle_error "Failed to download Composer installer" + php composer-setup.php --quiet || handle_error "Failed to install Composer" + rm composer-setup.php || handle_error "Failed to remove Composer installer" + mv composer.phar /usr/local/bin/composer || handle_error "Failed to move Composer to /usr/local/bin" + chmod +x /usr/local/bin/composer || handle_error "Failed to make Composer executable" +fi +COMPLETED_STEPS+=("Composer installed") + +# Configure Nginx +log_step "Configuring Nginx" +mv /etc/nginx/sites-available/default /etc/nginx/sites-available/default.bak || handle_error "Failed to backup default Nginx site" +touch /etc/nginx/sites-available/default || handle_error "Failed to create new Nginx site" +ln -sf /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default || handle_error "Failed to symlink Nginx site" + +cat > /etc/nginx/sites-available/default << EOL +server { + listen 80; + listen [::]:80; + + root /var/www/html/public; + index index.php index.html index.htm index.nginx-debian.html; + + server_name $DOMAIN; + + location / { + try_files \$uri \$uri/ /index.php?\$query_string; + } + + location ~ \.php$ { + include snippets/fastcgi-php.conf; + fastcgi_pass unix:/var/run/php/php${PHP_VERSION}-fpm.sock; + } + + location ~ /\.ht { + deny all; + } +} +EOL +COMPLETED_STEPS+=("Nginx configured") + +# Configure SSL with Certbot +log_step "Configuring SSL with Certbot" +echo "Configuring SSL certificate for $DOMAIN" + +if [ -d "/etc/letsencrypt/live/$DOMAIN" ]; then + log_info "SSL certificate already exists for $DOMAIN, reusing existing certificate" + echo -e "1\n" | certbot --nginx -d "$DOMAIN" || handle_error "Failed to reuse SSL certificate" + COMPLETED_STEPS+=("SSL certificate reused (already exists)") +else + log_info "No existing SSL certificate found for $DOMAIN, requesting new certificate" + certbot --nginx -d "$DOMAIN" --agree-tos --email "$EMAIL_ADDRESS" --no-eff-email --non-interactive --redirect || handle_error "Failed to configure SSL with Certbot" + COMPLETED_STEPS+=("SSL configured with Certbot") +fi + +# Test the Nginx configuration (Graceful restart applied) +log_step "Restarting Nginx" +nginx -t || handle_error "Nginx configuration failed" +systemctl restart nginx || handle_error "Failed to restart Nginx" +COMPLETED_STEPS+=("Nginx restarted gracefully") + +# Setup Laravel application +log_step "Setting up Laravel application" +LOCAL_PATH="/var/www/html" +REPO_URL="$GITHUB_REPO_URL" + +if [ -d "$LOCAL_PATH" ]; then + rm -rf "$LOCAL_PATH" || handle_error "Failed to remove existing web root" +fi + +git clone -b master "$REPO_URL" "$LOCAL_PATH" || handle_error "Failed to clone repository" +cd "$LOCAL_PATH" || handle_error "Failed to change directory to web root" + +# Install Laravel dependencies +log_step "Installing Laravel dependencies" +composer install --no-interaction || handle_error "Failed to install Laravel dependencies" +COMPLETED_STEPS+=("Laravel dependencies installed") + +# Create and configure .env file +log_step "Configuring .env file" +cp .env.example .env || handle_error "Failed to copy .env.example to .env" +php artisan key:generate --force || handle_error "Failed to generate Laravel key" +COMPLETED_STEPS+=(".env file configured") + +# Update .env with database credentials +sed -i "s|DB_HOST=.*|DB_HOST=localhost|" .env || handle_error "Failed to update DB_HOST in .env" +sed -i "s|DB_PORT=.*|DB_PORT=3306|" .env || handle_error "Failed to update DB_PORT in .env" +sed -i "s|DB_DATABASE=.*|DB_DATABASE=$MYSQL_DATABASE|" .env || handle_error "Failed to update DB_DATABASE in .env" +sed -i "s|DB_USERNAME=.*|DB_USERNAME=$MYSQL_USER|" .env || handle_error "Failed to update DB_USERNAME in .env" +sed -i "s|DB_PASSWORD=.*|DB_PASSWORD=$MYSQL_PASSWORD|" .env || handle_error "Failed to update DB_PASSWORD in .env" +sed -i "s|APP_URL=.*|APP_URL=https://$DOMAIN|" .env || handle_error "Failed to update APP_URL in .env" +sed -i "s|APP_NAME=.*|APP_NAME=\"$DOMAIN\"|" .env || handle_error "Failed to update APP_NAME in .env" +COMPLETED_STEPS+=(".env file updated with database credentials") + +# Run Laravel migrations and seed the database +log_step "Running Laravel migrations and seeding database" +php artisan migrate --force || handle_error "Failed to run Laravel migrations" +php artisan db:seed --force || handle_error "Failed to seed database" +COMPLETED_STEPS+=("Laravel migrations run and database seeded") + +# Set correct www permissions +log_step "Setting correct www permissions" +chown -R www-data:www-data /var/www/html || handle_error "Failed to set ownership of web root" +chmod -R 775 /var/www/html/storage || handle_error "Failed to set permissions of storage directory" +chmod -R 775 /var/www/html/bootstrap/cache || handle_error "Failed to set permissions of cache directory" + +# Make the scripts executable +chmod +x /var/www/html/sh/set_permissions.sh || handle_error "Failed to make permissions script executable" +chmod +x /var/www/html/sh/restart-services.sh || handle_error "Failed to make services script executable" + +# Run the script once to apply initial configuration +/var/www/html/sh/set_permissions.sh || handle_error "Failed to run permissions script" +/var/www/html/sh/restart-services.sh || handle_error "Failed to run services script" +COMPLETED_STEPS+=("Correct www permissions set") + +# Optimize RADIUS database indexes +log_step "Optimizing RADIUS database indexes" +cat > /tmp/radius_optimize.sql << "EOL" +USE radius; +ALTER TABLE radcheck ADD INDEX idx_username (username), ADD INDEX idx_attribute (attribute); +ANALYZE TABLE radcheck; +ALTER TABLE radreply ADD INDEX idx_username (username), ADD INDEX idx_attribute (attribute); +ANALYZE TABLE radreply; +ALTER TABLE radusergroup ADD INDEX idx_username (username), ADD INDEX idx_groupname (groupname); +ANALYZE TABLE radusergroup; +ALTER TABLE radgroupcheck ADD INDEX idx_groupname (groupname), ADD INDEX idx_attribute (attribute); +ANALYZE TABLE radgroupcheck; +ALTER TABLE radgroupreply ADD INDEX idx_groupname (groupname), ADD INDEX idx_attribute (attribute); +ANALYZE TABLE radgroupreply; +ALTER TABLE radacct ADD INDEX idx_username (username), ADD INDEX idx_acctsessionid (acctsessionid), ADD INDEX idx_framedipaddress (framedipaddress), ADD INDEX idx_acctstarttime (acctstarttime), ADD INDEX idx_acctstoptime (acctstoptime), ADD INDEX idx_nasipaddress (nasipaddress), ADD INDEX idx_calledstationid (calledstationid), ADD INDEX idx_callingstationid (callingstationid); +ANALYZE TABLE radacct; +ALTER TABLE radpostauth ADD INDEX idx_username (username), ADD INDEX idx_reply (reply), ADD INDEX idx_authdate (authdate); +ANALYZE TABLE radpostauth; +ALTER TABLE radcheck ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE radreply ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE radusergroup ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE radgroupcheck ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE radgroupreply ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE radacct ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE radpostauth ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ANALYZE TABLE radcheck; +ANALYZE TABLE radreply; +ANALYZE TABLE radusergroup; +ANALYZE TABLE radgroupcheck; +ANALYZE TABLE radgroupreply; +ANALYZE TABLE radacct; +ANALYZE TABLE radpostauth; +EOL + +mysql -u root < /tmp/radius_optimize.sql || handle_error "Failed to optimize RADIUS database indexes" +rm -f /tmp/radius_optimize.sql || handle_error "Failed to remove radius optimization sql file" +COMPLETED_STEPS+=("RADIUS database indexes optimized") + +# Configure Supervisor for queue worker (Pre-created log dir fix) +log_step "Configuring Supervisor for queue worker" +mkdir -p /var/www/html/storage/logs || handle_error "Failed to create Laravel logs directory" +cat > /etc/supervisor/conf.d/queue-worker.conf << "EOL" +[program:queue-worker] +process_name=%(program_name)s_%(process_num)02d +command=php /var/www/html/artisan queue:work --tries=3 +autostart=true +autorestart=true +stopasgroup=true +killasgroup=true +user=www-data +numprocs=5 +redirect_stderr=true +stdout_logfile=/var/www/html/storage/logs/queue-worker.log +EOL +COMPLETED_STEPS+=("Supervisor configured for queue worker") + +# Install OpenVPN based on Ubuntu version +log_step "Installing OpenVPN" +case $UBUNTU_VERSION in + "focal"|"jammy"|"noble") + echo "Installing OpenVPN for Ubuntu $UBUNTU_VERSION" + export AUTO_INSTALL=y + curl -O https://raw.githubusercontent.com/simpleisp/bash/main/openvpn.sh || handle_error "Failed to download OpenVPN installer" + chmod +x openvpn.sh || handle_error "Failed to make OpenVPN installer executable" + ./openvpn.sh || handle_error "Failed to install OpenVPN" + + # Set more secure permissions for OpenVPN + chown -R root:root /etc/openvpn || handle_error "Failed to set ownership of OpenVPN configuration directory" + chmod -R 777 /etc/openvpn || handle_error "Failed to set permissions of OpenVPN configuration directory" + chmod -R 777 /etc/openvpn/easy-rsa || handle_error "Failed to set permissions of OpenVPN easy-rsa directory" + ;; + *) + handle_error "Unsupported Ubuntu version for OpenVPN installation" + ;; +esac +COMPLETED_STEPS+=("OpenVPN installed") + +# Configure Systemd sandbox overrides for OpenVPN writes +log_step "Configuring Systemd sandbox overrides for OpenVPN writes" +mkdir -p /etc/systemd/system/php${PHP_VERSION}-fpm.service.d || handle_error "Failed to create PHP systemd override directory" +cat > /etc/systemd/system/php${PHP_VERSION}-fpm.service.d/override.conf << 'EOF' +[Service] +ReadWritePaths=/etc/openvpn +EOF + +mkdir -p /etc/systemd/system/supervisor.service.d || handle_error "Failed to create Supervisor systemd override directory" +cat > /etc/systemd/system/supervisor.service.d/override.conf << 'EOF' +[Service] +ReadWritePaths=/etc/openvpn +EOF +COMPLETED_STEPS+=("Systemd sandbox overrides configured") + +# Install Laravel cron (Safe Append Fix) +log_step "Installing cron" +(crontab -l 2>/dev/null; echo "* * * * * php /var/www/html/artisan schedule:run >> /dev/null 2>&1") | crontab - || handle_error "Failed to install Laravel cron job" +COMPLETED_STEPS+=("Cron job installed") + +# Update sudoers for www-data user (Updated openvpn explicit target) +log_step "Updating sudoers for www-data user" +cat >> /etc/sudoers << 'EOL' +www-data ALL=NOPASSWD: /bin/systemctl start openvpn@server +www-data ALL=NOPASSWD: /bin/systemctl stop openvpn@server +www-data ALL=NOPASSWD: /bin/systemctl restart openvpn@server +www-data ALL=NOPASSWD: /bin/systemctl status openvpn@server +www-data ALL=NOPASSWD: /bin/systemctl reload openvpn@server +www-data ALL=NOPASSWD: /bin/systemctl enable openvpn@server +www-data ALL=NOPASSWD: /bin/systemctl disable openvpn@server +www-data ALL=NOPASSWD: /bin/systemctl start freeradius +www-data ALL=NOPASSWD: /bin/systemctl stop freeradius +www-data ALL=NOPASSWD: /bin/systemctl restart freeradius +www-data ALL=NOPASSWD: /bin/systemctl status freeradius +www-data ALL=NOPASSWD: /bin/systemctl reload freeradius +www-data ALL=NOPASSWD: /bin/systemctl enable freeradius +www-data ALL=NOPASSWD: /bin/systemctl disable freeradius +www-data ALL=NOPASSWD: /bin/supervisorctl stop all +www-data ALL=NOPASSWD: /bin/supervisorctl reread +www-data ALL=NOPASSWD: /bin/supervisorctl update +www-data ALL=NOPASSWD: /bin/supervisorctl start all +www-data ALL=NOPASSWD: /bin/supervisorctl restart all +www-data ALL=NOPASSWD: /bin/supervisorctl status +www-data ALL=NOPASSWD: /bin/systemctl restart supervisor +www-data ALL=NOPASSWD: /bin/systemctl status ssh +www-data ALL=NOPASSWD: /var/www/html/sh/set_permissions.sh +www-data ALL=NOPASSWD: /var/www/html/sh/restart-services.sh +EOL +COMPLETED_STEPS+=("Sudoers updated for www-data user") + +# Open Firewall Ports and enable ufw +log_step "Opening firewall ports and enabling ufw" +ufw allow ssh || handle_error "Failed to allow SSH through firewall" +ufw allow 9080/tcp || handle_error "Failed to allow port 9080 through firewall" +ufw allow http || handle_error "Failed to allow HTTP through firewall" +ufw allow https || handle_error "Failed to allow HTTPS through firewall" +ufw allow 1194/tcp || handle_error "Failed to allow OpenVPN through firewall" +ufw allow 1812:1813/udp || handle_error "Failed to allow FreeRADIUS through firewall" +ufw reload || handle_error "Failed to reload firewall rules" +yes | ufw enable || handle_error "Failed to enable firewall" +COMPLETED_STEPS+=("Firewall ports opened and ufw enabled") + +# Test FreeRADIUS configuration +log_step "Checking FreeRADIUS files" + +if [ ! -f "/etc/freeradius/radiusd.conf" ]; then + log_info "FreeRADIUS configuration files missing, reinstalling and reconfiguring FreeRADIUS package" + apt-get purge -y freeradius freeradius-common freeradius-config 2>/dev/null || echo "FreeRADIUS not installed" + apt-get autoremove -y 2>/dev/null + apt-get install -y freeradius freeradius-mysql freeradius-config || handle_error "Failed to reinstall FreeRADIUS" + dpkg-reconfigure -f noninteractive freeradius-config 2>/dev/null || echo "Reconfigure not needed" +fi + +if [ ! -f "/etc/freeradius/radiusd.conf" ]; then + log_info "Creating minimal radiusd.conf configuration" + mkdir -p /etc/freeradius || handle_error "Failed to create freeradius config dir" + cat > /etc/freeradius/radiusd.conf << 'EOF' +prefix = /usr +exec_prefix = ${prefix} +sysconfdir = /etc +localstatedir = /var +sbindir = ${exec_prefix}/sbin +logdir = /var/log/freeradius +raddbdir = /etc/freeradius +radacctdir = ${logdir}/radacct + +name = freeradius +confdir = ${raddbdir} +modconfdir = ${confdir}/mods-config +certdir = ${confdir}/certs +cadir = ${confdir}/certs +run_dir = ${localstatedir}/run/${name} + +db_dir = ${raddbdir} +libdir = /usr/lib/freeradius +pidfile = ${run_dir}/${name}.pid + +correct_escapes = true +max_request_time = 30 +cleanup_delay = 5 +max_requests = 16384 +hostname_lookups = no + +log { +destination = files +colourise = yes +file = ${logdir}/radius.log +syslog_facility = daemon +stripped_names = no +auth = no +auth_badpass = no +auth_goodpass = no +msg_denied = "You are already logged in - access denied" +} + +checkrad = ${sbindir}/checkrad + +security { +allow_core_dumps = no +max_attributes = 200 +reject_delay = 1 +status_server = yes +} + +proxy_requests = yes +$INCLUDE proxy.conf +$INCLUDE clients.conf + +thread pool { +start_servers = 5 +max_servers = 32 +min_spare_servers = 3 +max_spare_servers = 10 +max_requests_per_server = 0 +auto_limit_acct = no +} + +$INCLUDE sites-enabled/ +$INCLUDE mods-enabled/ + +policy { +$INCLUDE policy.d/ +} + +instantiate { +} +EOF + chmod 644 /etc/freeradius/radiusd.conf || handle_error "Failed to set radiusd.conf permissions" + log_success "Created minimal radiusd.conf configuration" +fi + +if [ -f "/etc/freeradius/mods-available/sql" ]; then + ln -sf /etc/freeradius/mods-available/sql /etc/freeradius/mods-enabled/ || handle_error "Failed to re-enable SQL module" +fi +COMPLETED_STEPS+=("Completed checking FreeRADIUS files") + +# Enable buffered-sql site +log_step "Enabling FreeRADIUS buffered-sql site" +mkdir -p /etc/freeradius/sites-enabled || handle_error "Failed to create FreeRADIUS sites-enabled directory" +ln -sf /etc/freeradius/sites-available/buffered-sql /etc/freeradius/sites-enabled/buffered-sql || handle_error "Failed to enable buffered-sql site" +COMPLETED_STEPS+=("FreeRADIUS buffered-sql site enabled") + +# Enable SQL module for FreeRADIUS +log_step "Enabling SQL module" +mkdir -p /etc/freeradius/mods-enabled || handle_error "Failed to create FreeRADIUS mods-enabled directory" +ln -sf /etc/freeradius/mods-available/sql /etc/freeradius/mods-enabled/sql || handle_error "Failed to enable SQL module" +COMPLETED_STEPS+=("FreeRADIUS SQL module enabled") + +# Write new FreeRADIUS SQL module +log_step "Writing new FreeRADIUS SQL module" +SQL_FILE="/etc/freeradius/mods-available/sql" +cat > "$SQL_FILE" < /tmp/new_accounting_block +accounting { +sql +exec +attr_filter.accounting_response +} +EOF + awk ' + BEGIN { skip = 0 } + /^accounting[ \t]*{/ { print_block = 1; print_file("/tmp/new_accounting_block"); skip = 1; next } + /^[ \t]*}/ { if (skip) { skip = 0; next } } + !skip { print } + function print_file(file) { + while ((getline line < file) > 0) print line; + close(file) + } + ' "$DEFAULT_SITE" > /tmp/tmp_site && mv /tmp/tmp_site "$DEFAULT_SITE" +else + handle_error "Default site configuration file not found" +fi +COMPLETED_STEPS+=("FreeRADIUS default site configured") + +# Apply Systemd Sandbox changes from the overrides +systemctl daemon-reload || handle_error "Failed to reload systemd daemon" + +# Enable and start all services (Consolidated Block) +log_step "Enabling and restarting all services" + +SERVICES=(nginx php${PHP_VERSION}-fpm supervisor openvpn@server freeradius valkey-server) +for service in "${SERVICES[@]}"; do + systemctl enable "$service" || handle_error "Failed to enable $service" + systemctl restart "$service" || handle_error "Failed to restart $service" +done +COMPLETED_STEPS+=("All services enabled and restarted") + +# Create a script to update memory configurations +cat > /usr/local/bin/update_memory_config.sh << 'EOL' +#!/usr/bin/env bash +# Optimized Co-hosted Tuning Script for SimpleSpot +# Balances MariaDB, Valkey, PHP-FPM, and FreeRADIUS. + +set -euo pipefail + +LOG_FILE="/var/log/memory_config_updates.log" +log() { echo "[$(date +'%F %T')] $1" >> "$LOG_FILE"; } + +log "Starting memory configuration update" + +# 1. Introspect System Resources +TOTAL_RAM_MB=$(free -m | awk '/^Mem:/{print $2}') +VCPUS=$(nproc --all) + +log "Detected RAM: ${TOTAL_RAM_MB}MB | CPU Cores: ${VCPUS}" + +# 2. Budget Memory +# OS Reserve: 15% or 1024MB, whichever is higher +OS_RESERVE_MB=$(( TOTAL_RAM_MB * 15 / 100 )) +[ "$OS_RESERVE_MB" -lt 1024 ] && OS_RESERVE_MB=1024 + +# MariaDB: 40% of total RAM +MARIADB_POOL_MB=$(( TOTAL_RAM_MB * 40 / 100 )) +[ "$MARIADB_POOL_MB" -lt 256 ] && MARIADB_POOL_MB=256 + +# Valkey/Redis: 15% of total RAM +VALKEY_MB=$(( TOTAL_RAM_MB * 15 / 100 )) +[ "$VALKEY_MB" -lt 128 ] && VALKEY_MB=128 + +# PHP-FPM: The remaining RAM +REMAINING_MB=$(( TOTAL_RAM_MB - OS_RESERVE_MB - MARIADB_POOL_MB - VALKEY_MB )) +[ "$REMAINING_MB" -lt 256 ] && REMAINING_MB=256 + +log "Budget -> OS: ${OS_RESERVE_MB}MB | DB: ${MARIADB_POOL_MB}MB | Cache: ${VALKEY_MB}MB | PHP: ${REMAINING_MB}MB" + +# 3. Apply MariaDB Tuning +MARIADB_CONF="/etc/mysql/mariadb.conf.d/99-autotune.cnf" +log "Tuning MariaDB..." +cat > "$MARIADB_CONF" < 1024 ? MARIADB_POOL_MB / 1024 : 1 )) +innodb_log_file_size = $(( MARIADB_POOL_MB / 4 ))M +max_connections = $(( VCPUS * 150 )) +tmp_table_size = 64M +max_heap_table_size = 64M +EOF +systemctl restart mariadb || service mysql restart + +# 4. Apply Valkey Tuning +VALKEY_CONF="/etc/valkey/valkey.conf" +if [ -f "$VALKEY_CONF" ]; then + log "Tuning Valkey..." + # Replace existing maxmemory line or append it + sed -i -E "s/^#?maxmemory .*/maxmemory ${VALKEY_MB}mb/" "$VALKEY_CONF" + # Ensure eviction policy is set safely for Laravel (protects queues/sessions without expirations) + if ! grep -q "^maxmemory-policy" "$VALKEY_CONF"; then + echo "maxmemory-policy volatile-lru" >> "$VALKEY_CONF" + fi + systemctl restart valkey-server || true +fi + +# 5. Apply PHP-FPM Tuning +# Assuming Laravel average process size = 80MB +AVG_PHP_RSS_MB=80 +MAX_CHILDREN=$(( REMAINING_MB / AVG_PHP_RSS_MB )) +[ "$MAX_CHILDREN" -lt 5 ] && MAX_CHILDREN=5 + +# Find active PHP-FPM pool file +POOL_CONF=$(find /etc/php/*/fpm/pool.d/ -name "www.conf" | head -n 1) +if [ -n "$POOL_CONF" ]; then + log "Tuning PHP-FPM in $POOL_CONF..." + sed -i -E "s/^[;#]?pm.max_children =.*/pm.max_children = ${MAX_CHILDREN}/" "$POOL_CONF" + sed -i -E "s/^[;#]?pm.start_servers =.*/pm.start_servers = $(( MAX_CHILDREN * 20 / 100 + 1 ))/" "$POOL_CONF" + sed -i -E "s/^[;#]?pm.min_spare_servers =.*/pm.min_spare_servers = $(( MAX_CHILDREN * 10 / 100 + 1 ))/" "$POOL_CONF" + sed -i -E "s/^[;#]?pm.max_spare_servers =.*/pm.max_spare_servers = $(( MAX_CHILDREN * 30 / 100 + 1 ))/" "$POOL_CONF" + + # Reload PHP-FPM + PHP_SVC=$(basename $(dirname $(dirname "$POOL_CONF")) | sed 's|^|php|;s|$|-fpm|') + systemctl reload "$PHP_SVC" || systemctl restart "$PHP_SVC" +fi + +# 6. Apply FreeRADIUS Tuning +RADIUS_CONF=$(find /etc/freeradius -name "radiusd.conf" 2>/dev/null | head -n 1) +if [ -n "$RADIUS_CONF" ]; then + log "Tuning FreeRADIUS in $RADIUS_CONF..." + # Start 2 threads per core, max out at 6 per core + START_SERVERS=$(( VCPUS * 2 )) + MAX_SERVERS=$(( VCPUS * 6 )) + + awk -v s="$START_SERVERS" -v m="$MAX_SERVERS" ' + BEGIN { inpool=0 } + /^thread pool \{/ { inpool=1; print; next } + /^\}/ { inpool=0; print; next } + inpool && /^[[:space:]]*start_servers/ { printf "\tstart_servers = %s\n", s; next } + inpool && /^[[:space:]]*max_servers/ { printf "\tmax_servers = %s\n", m; next } + { print } + ' "$RADIUS_CONF" > "${RADIUS_CONF}.tmp" && mv "${RADIUS_CONF}.tmp" "$RADIUS_CONF" + + systemctl restart freeradius || true +fi + +log "Memory configuration update completed" +EOL + +# Make the script executable +chmod +x /usr/local/bin/update_memory_config.sh || handle_error "Failed to make memory config script executable" + +# Add cron job to run at 3 AM daily (Safe Append Fix applied) +(crontab -l 2>/dev/null; echo "0 3 * * * /usr/local/bin/update_memory_config.sh") | crontab - || handle_error "Failed to add cron job" + +# Run the script once to apply initial configuration +/usr/local/bin/update_memory_config.sh || handle_error "Failed to run initial memory configuration" +COMPLETED_STEPS+=("Automatic memory configuration script installed at /usr/local/bin/update_memory_config.sh") + +# Final verification +log_step "Verifying all services are running" +for service in nginx mariadb freeradius valkey-server php${PHP_VERSION}-fpm; do + if ! systemctl is-active --quiet $service; then + log_warning "$service is not running" + systemctl status $service + else + log_success "$service is running" + fi +done + +# Complete installation message +log_success "Installation completed successfully!" +echo "You can find your database credentials in $DB_CREDENTIALS_FILE" +echo "Your SimpleSpot installation is available at: https://$DOMAIN" \ No newline at end of file diff --git a/universal.sh b/universal.sh index 1b95b0e..c0eae72 100644 --- a/universal.sh +++ b/universal.sh @@ -366,7 +366,7 @@ log "------------------------" TOTAL_MEM_KB=$(awk '/MemTotal/ {print $2}' /proc/meminfo 2>/dev/null || echo $((RAM_MB*1024))) TOTAL_MEM_BYTES=$((TOTAL_MEM_KB * 1024)) MIN_POOL_BYTES=$((1 * 1024 * 1024 * 1024)) -CALC_POOL_BYTES=$(awk -v m="$TOTAL_MEM_BYTES" 'BEGIN{printf("%d", m*0.50)}') +CALC_POOL_BYTES=$(awk -v m="$TOTAL_MEM_BYTES" 'BEGIN{printf("%d", m*0.40)}') INNODB_BUFFER_POOL_SIZE_BYTES=$(( CALC_POOL_BYTES < MIN_POOL_BYTES ? MIN_POOL_BYTES : CALC_POOL_BYTES )) ROUND_128MB=$((128 * 1024 * 1024)) INNODB_BUFFER_POOL_SIZE_BYTES=$(( (INNODB_BUFFER_POOL_SIZE_BYTES / ROUND_128MB) * ROUND_128MB )) @@ -438,6 +438,36 @@ fi log "" +# ----------------------------------------------------- +# Step 5b: Compute and apply Valkey tuning +# ----------------------------------------------------- +log "========================" +log " Step 5b: Valkey tuning (maxmemory & volatile-lru)" +log "------------------------" + +VALKEY_CONF="/etc/valkey/valkey.conf" +if [ -f "$VALKEY_CONF" ]; then + VALKEY_MB=$(( RAM_MB * 15 / 100 )) + [ "$VALKEY_MB" -lt 128 ] && VALKEY_MB=128 + + log "[INFO] Valkey maxmemory: ${VALKEY_MB}M" + + if [ "$DRY_RUN" -eq 0 ]; then + cp -a "$VALKEY_CONF" "${VALKEY_CONF}.bak.${TIMESTAMP}" + sed -i -E "s/^#?maxmemory .*/maxmemory ${VALKEY_MB}mb/" "$VALKEY_CONF" + if ! grep -q "^maxmemory-policy" "$VALKEY_CONF"; then + echo "maxmemory-policy volatile-lru" >> "$VALKEY_CONF" + fi + log "[OK] Valkey configured with maxmemory ${VALKEY_MB}mb and volatile-lru" + else + log "[DRY] would tune Valkey maxmemory to ${VALKEY_MB}mb and volatile-lru" + fi +else + log "[WARN] Valkey config not found at $VALKEY_CONF, skipping" +fi + +log "" + # ------------------------------------------------------------------ # Step 6: FreeRADIUS thread pool tuning in radiusd.conf (backup first) # ------------------------------------------------------------------ @@ -617,9 +647,15 @@ else # Compute recommended values CPU_CORES=$(nproc --all 2>/dev/null || echo 1) TOTAL_RAM_MB=$(free -m | awk '/^Mem:/{print $2}') - RESERVE_BY_PERCENT=$(awk -v r="$TOTAL_RAM_MB" 'BEGIN{printf "%.0f", r*0.15}') - RESERVE_MB=$(( RESERVE_BY_PERCENT + CPU_CORES*200 )) - [ "$RESERVE_MB" -lt 1024 ] && RESERVE_MB=1024 + # Match the global budget strategy: 15% OS + 40% DB + 15% Cache + OS_RESERVE_MB=$(( TOTAL_RAM_MB * 15 / 100 )) + [ "$OS_RESERVE_MB" -lt 1024 ] && OS_RESERVE_MB=1024 + MARIADB_POOL_MB=$(( TOTAL_RAM_MB * 40 / 100 )) + [ "$MARIADB_POOL_MB" -lt 256 ] && MARIADB_POOL_MB=256 + VALKEY_MB=$(( TOTAL_RAM_MB * 15 / 100 )) + [ "$VALKEY_MB" -lt 128 ] && VALKEY_MB=128 + + RESERVE_MB=$(( OS_RESERVE_MB + MARIADB_POOL_MB + VALKEY_MB )) AVAILABLE_FOR_PHP_MB=$(( TOTAL_RAM_MB - RESERVE_MB )) [ "$AVAILABLE_FOR_PHP_MB" -lt 256 ] && AVAILABLE_FOR_PHP_MB=256 @@ -833,6 +869,17 @@ if [ "$RAD_RESTARTED" -eq 0 ]; then log "[WARN] FreeRADIUS restart not confirmed; check OS packaging (freeradius or radiusd)" fi +VALKEY_RESTARTED=0 +for candidate in valkey-server valkey; do + if restart_and_check "$candidate"; then + VALKEY_RESTARTED=1 + break + fi +done +if [ "$VALKEY_RESTARTED" -eq 0 ]; then + log "[WARN] Valkey restart not confirmed; check OS packaging" +fi + if [ -n "${PHPFPM_SERVICE:-}" ]; then restart_and_check "$PHPFPM_SERVICE" fi From 837e7937d0447b59067c9fa06010110d7392a056 Mon Sep 17 00:00:00 2001 From: Barnabie Opiyo Date: Thu, 28 May 2026 02:06:48 +0300 Subject: [PATCH 16/45] feat: add database cleanup script and enforce LF line endings across shell scripts to resolve encoding issues. --- .gitattributes | 3 + clean_server.sh | 2 +- db_cleanup.sh | 142 ++++++++++ install.sh | 606 ++++++++++++++++++++--------------------- openvpn.sh | 14 +- ports.sh | 6 +- setup.sh | 2 +- template_generation.sh | 22 +- transfer_tmpl.sh | 2 +- ubuntu_simpleisp.sh | 10 +- ubuntu_simplespot.sh | 10 +- universal.sh | 12 +- 12 files changed, 488 insertions(+), 343 deletions(-) create mode 100644 .gitattributes create mode 100644 db_cleanup.sh diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..c4cb8ca --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +# Enforce Unix line endings (LF) for shell scripts to prevent +# "cannot execute: required file not found" errors on Linux servers. +*.sh text eol=lf diff --git a/clean_server.sh b/clean_server.sh index 7043700..ec001d7 100644 --- a/clean_server.sh +++ b/clean_server.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/bash # Cleanup script for SimpleISP/SimpleSpot # This script will uninstall all software installed by the SimpleISP/SimpleSpot installer diff --git a/db_cleanup.sh b/db_cleanup.sh new file mode 100644 index 0000000..5f8e547 --- /dev/null +++ b/db_cleanup.sh @@ -0,0 +1,142 @@ +#!/bin/bash + +# Configuration +DB_NAME="radius" +DB_USER="root" +MYSQL_BIN=$(which mysql) +LOG_FILE="/var/log/radius_db_cleanup.log" + +# Ensure log file exists and is writable +touch "$LOG_FILE" 2>/dev/null + +# Logging function to guarantee clean output to screen and file +log_message() { + local message="$1" + echo "$message" + echo "$message" >> "$LOG_FILE" +} + +log_message "=========================================================" +log_message "Starting Database Maintenance Run: $(date)" +log_message "=========================================================" + +# Initialize dynamic counter variables for reporting +TOTAL_RADACCT=0 +TOTAL_RADPOSTAUTH=0 +TOTAL_HOTSPOT_SESSIONS=0 +TOTAL_VOUCHERS_EXPIRED=0 +TOTAL_HOTSPOT_PAYMENTS=0 +TOTAL_MPESA_STKS=0 + +# NEW: Unified helper function to delete and count in the exact same session +execute_and_count() { + local query="$1" + local result + + # Run the query and append SELECT ROW_COUNT() in the same transaction + result=$($MYSQL_BIN -u "$DB_USER" "$DB_NAME" -N -e "$query; SELECT ROW_COUNT();" 2>/dev/null) + + # Ensure the result is a valid number, otherwise return 0 + if [[ ! "$result" =~ ^[0-9]+$ ]]; then + echo 0 + else + echo "$result" + fi +} + +# --------------------------------------------------------- +# 1. RADIUS AAA Operational Tables (1-Week Window) +# --------------------------------------------------------- +log_message "Cleaning up core RADIUS operational logs older than 1 week..." + +RADACCT_QUERY="DELETE FROM radacct WHERE acctstoptime IS NOT NULL AND acctstoptime < NOW() - INTERVAL 1 WEEK LIMIT 10000" + +while true; do + ROWS=$(execute_and_count "$RADACCT_QUERY") + if [ "$ROWS" -le 0 ]; then break; fi + TOTAL_RADACCT=$((TOTAL_RADACCT + ROWS)) + sleep 0.2 +done +log_message " -> radacct processing complete." + +RADPOSTAUTH_QUERY="DELETE FROM radpostauth WHERE authdate < NOW() - INTERVAL 1 WEEK LIMIT 10000" + +while true; do + ROWS=$(execute_and_count "$RADPOSTAUTH_QUERY") + if [ "$ROWS" -le 0 ]; then break; fi + TOTAL_RADPOSTAUTH=$((TOTAL_RADPOSTAUTH + ROWS)) + sleep 0.2 +done +log_message " -> radpostauth processing complete." + + +# --------------------------------------------------------- +# 2. Hotspot App Sessions & Depleted Vouchers (2-Day Window) +# --------------------------------------------------------- +log_message "Cleaning up captive portal records and depleted vouchers..." + +HOTSPOT_SESSION_QUERY="DELETE FROM hotspot_sessions WHERE expires_at < NOW() - INTERVAL 1 WEEK LIMIT 5000" + +while true; do + ROWS=$(execute_and_count "$HOTSPOT_SESSION_QUERY") + if [ "$ROWS" -le 0 ]; then break; fi + TOTAL_HOTSPOT_SESSIONS=$((TOTAL_HOTSPOT_SESSIONS + ROWS)) + sleep 0.1 +done +log_message " -> hotspot_sessions processing complete." + +VOUCHER_STATUS_CLEANUP="DELETE FROM vouchers WHERE status = 'expired' AND expiration_time < NOW() - INTERVAL 2 DAY" +ROWS=$(execute_and_count "$VOUCHER_STATUS_CLEANUP") +TOTAL_VOUCHERS_EXPIRED=$((TOTAL_VOUCHERS_EXPIRED + ROWS)) +log_message " -> Vouchers expired/depleted for more than 2 days cleared." + + +# --------------------------------------------------------- +# 3. Application Payment Ledgers (6-Month Retention Window) +# --------------------------------------------------------- +log_message "Cleaning up payment transactions older than 6 months..." + +PAYMENT_CLEANUP_QUERY="DELETE FROM hotspot_payments WHERE created_at < NOW() - INTERVAL 6 MONTH LIMIT 5000" + +while true; do + ROWS=$(execute_and_count "$PAYMENT_CLEANUP_QUERY") + if [ "$ROWS" -le 0 ]; then break; fi + TOTAL_HOTSPOT_PAYMENTS=$((TOTAL_HOTSPOT_PAYMENTS + ROWS)) + sleep 0.1 +done +log_message " -> hotspot_payments processing complete." + +STK_CLEANUP_QUERY="DELETE FROM mpesa_stks WHERE created_at < NOW() - INTERVAL 6 MONTH" +ROWS=$(execute_and_count "$STK_CLEANUP_QUERY") +TOTAL_MPESA_STKS=$((TOTAL_MPESA_STKS + ROWS)) +log_message " -> mpesa_stks processing complete." + + +# --------------------------------------------------------- +# 4. Final Maintenance Summary Dashboard +# --------------------------------------------------------- +log_message "" +log_message "=========================================================" +log_message " MAINTENANCE CLEANUP SUMMARY REPORT " +log_message "=========================================================" +log_message "Execution Timestamp : $(date)" +log_message "Target Database : $DB_NAME" +log_message "---------------------------------------------------------" +LINE=$(printf "%-25s | %-15s | %-15s" "Table Name" "Retention Pol" "Records Cleared") +log_message "$LINE" +log_message "---------------------------------------------------------" +LINE=$(printf "%-25s | %-15s | %-15s" "radacct" "1 Week" "$TOTAL_RADACCT") +log_message "$LINE" +LINE=$(printf "%-25s | %-15s | %-15s" "radpostauth" "1 Week" "$TOTAL_RADPOSTAUTH") +log_message "$LINE" +LINE=$(printf "%-25s | %-15s | %-15s" "hotspot_sessions" "1 Week" "$TOTAL_HOTSPOT_SESSIONS") +log_message "$LINE" +LINE=$(printf "%-25s | %-15s | %-15s" "vouchers (Status=Expired)" "2 Days" "$TOTAL_VOUCHERS_EXPIRED") +log_message "$LINE" +LINE=$(printf "%-25s | %-15s | %-15s" "hotspot_payments" "6 Months" "$TOTAL_HOTSPOT_PAYMENTS") +log_message "$LINE" +LINE=$(printf "%-25s | %-15s | %-15s" "mpesa_stks" "6 Months" "$TOTAL_MPESA_STKS") +log_message "$LINE" +log_message "=========================================================" +log_message "Database Maintenance Completed Successfully." +log_message "=========================================================" \ No newline at end of file diff --git a/install.sh b/install.sh index ec23b37..cc78e22 100644 --- a/install.sh +++ b/install.sh @@ -1,303 +1,303 @@ -#!/bin/bash - -# Install MariaDB without a password -export DEBIAN_FRONTEND=noninteractive -sudo debconf-set-selections <<< "mariadb-server-10.3 mysql-server/root_password password ''" -sudo debconf-set-selections <<< "mariadb-server-10.3 mysql-server/root_password_again password ''" -sudo apt-get install -y mariadb-server - -# Change the root user's authentication plugin to unix_socket -sudo mysql -e "USE mysql; UPDATE user SET plugin='unix_socket' WHERE User='root'; FLUSH PRIVILEGES;" - -# Generate random MySQL username and password -MYSQL_USER="user_$(openssl rand -hex 3)" -MYSQL_PASSWORD="$(openssl rand -base64 12)" - -# Login to MariaDB and setup -sudo mysql -e " -CREATE DATABASE radius; -CREATE USER '${MYSQL_USER}'@'localhost' IDENTIFIED BY '${MYSQL_PASSWORD}'; -GRANT ALL PRIVILEGES ON radius.* TO '${MYSQL_USER}'@'localhost'; -GRANT CREATE ROUTINE, ALTER ROUTINE ON radius.* TO '${MYSQL_USER}'@'localhost'; -FLUSH PRIVILEGES; -" - -# Set the MySQL timezone for the 'radius' database -# sudo mysql -e "USE radius; SET GLOBAL time_zone = '+03:00'; SET time_zone = '+03:00';" -sudo mysql -e "SET GLOBAL time_zone = '+03:00'; SET time_zone = '+03:00';" - -# Nginx Installation - -# Step 1: Update the system -sudo apt update - -# Step 2: Install Nginx -sudo apt install -y nginx - -# Adjusting the firewall -sudo ufw allow http -sudo ufw allow https - -# Step 3: Prompt user for domain name -read -p "Enter your domain name (e.g., example.com): " domain_name - -# Step 4: Configure Nginx for Laravel -sudo bash -c 'cat > /etc/nginx/sites-available/default < /dev/null; then - echo "Composer not found. Installing Composer..." - php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" - php composer-setup.php --quiet - rm composer-setup.php - mv composer.phar /usr/local/bin/composer - chmod +x /usr/local/bin/composer -fi - -# Proceed with the clone only if the /var/www/html/public directory doesn't exist or the index.php file is not present inside it -if [ ! -d "$LOCAL_PATH/public" ] || [ ! -f "$LOCAL_PATH/public/index.php" ]; then - # Move the existing /var/www/html to a temporary location - TEMP_PATH="/tmp/var_www_html_backup" - if [ -d "$LOCAL_PATH" ]; then - mv "$LOCAL_PATH" "$TEMP_PATH" - fi - - # Clone the repository into the local Laravel application directory - git clone "$REPO_URL" "$LOCAL_PATH" || { echo "Cloning failed. Restoring original /var/www/html and exiting."; mv "$TEMP_PATH" "$LOCAL_PATH"; exit 1; } - - # Remove the temporary backup directory if it exists - if [ -d "$TEMP_PATH" ]; then - rm -rf "$TEMP_PATH" - fi - - # Generate a new application key - php "$LOCAL_PATH"/artisan key:generate --force - - # Clear application cache - php "$LOCAL_PATH"/artisan cache:clear - - # Clear route cache - php "$LOCAL_PATH"/artisan route:clear - - # Clear config cache - php "$LOCAL_PATH"/artisan config:clear - - # Clear compiled views cache - php "$LOCAL_PATH"/artisan view:clear - - # # Optimize the application - # php "$LOCAL_PATH"/artisan optimize - -else - echo "The /var/www/html/public directory exists and contains index.php. Skipping clone and setup." -fi - -# Install FreeRADIUS and FreeRADIUS-MySQL -sudo apt-get install -y freeradius freeradius-mysql freeradius-utils - -# # Specify the path to the FreeRADIUS SQL file -sql_file="/etc/freeradius/3.0/mods-available/sql" - - -# Replace files -sed -i "s/login = .*/login = \"$MYSQL_USER\"/" "$sql_file" -sed -i "s/password = .*/password = \"$MYSQL_PASSWORD\"/" "$sql_file" -# sed -i "s/read_clients = .*/read_clients = yes/" "$sql_file" - -# # Enable SQL module and configure FreeRADIUS to use it -sudo ln -s /etc/freeradius/3.0/mods-available/sql /etc/freeradius/3.0/mods-enabled/ - -# # Enable & Restart FreeRADIUS service -systemctl enable freeradius.service -sudo systemctl restart freeradius.service - -# Install OpenVPN -sudo apt-get update - -# Set the environment variable -export AUTO_INSTALL=y - -# Download the openvpn.sh script -curl -O https://raw.githubusercontent.com/simpleisp/bash/main/openvpn.sh - -# Make the openvpn.sh script executable -chmod +x openvpn.sh - -# Run the openvpn.sh script -./openvpn.sh - -# Add an OpenVPN configuration file if needed, e.g.: -# sudo cp /path/to/your/openvpn/config.ovpn /etc/openvpn/ - -# Enable and start OpenVPN service -sudo systemctl enable openvpn -sudo systemctl start openvpn -sudo apt install -y easy-rsa - -# Set permissions for /etc/openvpn -sudo chmod -R 777 /etc/openvpn -sudo chmod -R 777 /etc/openvpn/easy-rsa -sudo ufw allow 1194/tcp - -# Install Php imap -apt install -y php-imap - -# Supervisor Installation - -# Step 1: Install Supervisor -sudo apt-get install -y supervisor - -# Step 2: Configure Supervisor -sudo bash -c "cat > /etc/supervisor/conf.d/queue-worker.conf << EOL -[program:queue-worker] -process_name=%(program_name)s_%(process_num)02d -command=php /var/www/html/artisan queue:work --tries=3 -autostart=true -autorestart=true -stopasgroup=true -killasgroup=true -user=www-data -numprocs=5 -redirect_stderr=true -stdout_logfile=/var/www/html/storage/logs/queue.log -stopwaitsecs=3600 -EOL" - -# Step 3: Start Supervisor -sudo systemctl start supervisor -sudo supervisorctl reread -sudo supervisorctl update -sudo supervisorctl start queue-worker:* - -# Update sudoers file to allow www-data user to restart and check the status of OpenVPN without a password -sudo bash -c "cat >> /etc/sudoers << EOL -www-data ALL=NOPASSWD: /bin/systemctl start openvpn -www-data ALL=NOPASSWD: /bin/systemctl stop openvpn -www-data ALL=NOPASSWD: /bin/systemctl restart openvpn -www-data ALL=NOPASSWD: /bin/systemctl status openvpn -www-data ALL=NOPASSWD: /bin/systemctl reload openvpn -www-data ALL=NOPASSWD: /bin/systemctl enable openvpn -www-data ALL=NOPASSWD: /bin/systemctl disable openvpn -www-data ALL=NOPASSWD: /bin/systemctl start freeradius -www-data ALL=NOPASSWD: /bin/systemctl stop freeradius -www-data ALL=NOPASSWD: /bin/systemctl restart freeradius -www-data ALL=NOPASSWD: /bin/systemctl status freeradius -www-data ALL=NOPASSWD: /bin/systemctl reload freeradius -www-data ALL=NOPASSWD: /bin/systemctl enable freeradius -www-data ALL=NOPASSWD: /bin/systemctl disable freeradius -www-data ALL=NOPASSWD: /bin/supervisorctl stop all -www-data ALL=NOPASSWD: /bin/supervisorctl reread -www-data ALL=NOPASSWD: /bin/supervisorctl update -www-data ALL=NOPASSWD: /bin/supervisorctl start all -www-data ALL=NOPASSWD: /bin/supervisorctl restart all -www-data ALL=NOPASSWD: /bin/supervisorctl status -www-data ALL=NOPASSWD: /bin/systemctl restart supervisor -www-data ALL=NOPASSWD: /bin/systemctl status ssh -www-data ALL=NOPASSWD: /var/www/html/sh/set_permissions.sh -www-data ALL=NOPASSWD: /var/www/html/sh/restart-services.sh -EOL" - -# Set permissions -sudo chown -R www-data:www-data /var/www -sudo chown -R www-data:www-data /var/www/html -sudo chmod -R 777 /var/www/html/storage -sudo chmod -R 777 /var/www/html/bootstrap/cache -sudo chown -R www-data:www-data /var/www/html/bootstrap/cache -sudo timedatectl set-timezone Africa/Nairobi -sudo ufw allow 1812:1813/udp - -# Make script executable -chmod +x /var/www/html/sh/set_permissions.sh -chmod +x /var/www/html/sh/restart-services.sh - -# Install cron -# Write cron job entry to a temporary file -echo "* * * * * php /var/www/html/artisan schedule:run >> /dev/null 2>&1" > cronjob - -# Install the cron job from the temporary file -crontab cronjob - -# Clean up the temporary file -rm cronjob - -# Step 5: Prompt user for email address and install Certbot -read -p "Enter your email address for certificate management: " email_address - -# Install Certbot -sudo apt install python3-certbot-nginx -y - - -# Run Certbot for the given domain -sudo certbot --nginx -d $domain_name --agree-tos --email $email_address --no-eff-email --non-interactive - -# Install Composer dependencies (initial setup) without human interaction -cd "$LOCAL_PATH" -composer install --no-interaction - - - -echo "MariaDB setup completed" -echo "ssl certificate issued" -echo "FreeRADIUS setup completed" -# Display the message to copy the details -echo "" -echo "**********************************************************" -echo "IMPORTANT: Please copy these details. You will need them to continue to the next step." -echo "**********************************************************" -echo "" -echo "Database name: radius" -echo "Generated MySQL username: ${MYSQL_USER}" -echo "Generated MySQL password: ${MYSQL_PASSWORD}" -echo "" -echo "**********************************************************" -echo "Installation completed." -echo "Please access the following link to finalize the setup:" -echo "https://$domain_name/install" -echo "**********************************************************" -echo "" +#!/bin/bash + +# Install MariaDB without a password +export DEBIAN_FRONTEND=noninteractive +sudo debconf-set-selections <<< "mariadb-server-10.3 mysql-server/root_password password ''" +sudo debconf-set-selections <<< "mariadb-server-10.3 mysql-server/root_password_again password ''" +sudo apt-get install -y mariadb-server + +# Change the root user's authentication plugin to unix_socket +sudo mysql -e "USE mysql; UPDATE user SET plugin='unix_socket' WHERE User='root'; FLUSH PRIVILEGES;" + +# Generate random MySQL username and password +MYSQL_USER="user_$(openssl rand -hex 3)" +MYSQL_PASSWORD="$(openssl rand -base64 12)" + +# Login to MariaDB and setup +sudo mysql -e " +CREATE DATABASE radius; +CREATE USER '${MYSQL_USER}'@'localhost' IDENTIFIED BY '${MYSQL_PASSWORD}'; +GRANT ALL PRIVILEGES ON radius.* TO '${MYSQL_USER}'@'localhost'; +GRANT CREATE ROUTINE, ALTER ROUTINE ON radius.* TO '${MYSQL_USER}'@'localhost'; +FLUSH PRIVILEGES; +" + +# Set the MySQL timezone for the 'radius' database +# sudo mysql -e "USE radius; SET GLOBAL time_zone = '+03:00'; SET time_zone = '+03:00';" +sudo mysql -e "SET GLOBAL time_zone = '+03:00'; SET time_zone = '+03:00';" + +# Nginx Installation + +# Step 1: Update the system +sudo apt update + +# Step 2: Install Nginx +sudo apt install -y nginx + +# Adjusting the firewall +sudo ufw allow http +sudo ufw allow https + +# Step 3: Prompt user for domain name +read -p "Enter your domain name (e.g., example.com): " domain_name + +# Step 4: Configure Nginx for Laravel +sudo bash -c 'cat > /etc/nginx/sites-available/default < /dev/null; then + echo "Composer not found. Installing Composer..." + php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" + php composer-setup.php --quiet + rm composer-setup.php + mv composer.phar /usr/local/bin/composer + chmod +x /usr/local/bin/composer +fi + +# Proceed with the clone only if the /var/www/html/public directory doesn't exist or the index.php file is not present inside it +if [ ! -d "$LOCAL_PATH/public" ] || [ ! -f "$LOCAL_PATH/public/index.php" ]; then + # Move the existing /var/www/html to a temporary location + TEMP_PATH="/tmp/var_www_html_backup" + if [ -d "$LOCAL_PATH" ]; then + mv "$LOCAL_PATH" "$TEMP_PATH" + fi + + # Clone the repository into the local Laravel application directory + git clone "$REPO_URL" "$LOCAL_PATH" || { echo "Cloning failed. Restoring original /var/www/html and exiting."; mv "$TEMP_PATH" "$LOCAL_PATH"; exit 1; } + + # Remove the temporary backup directory if it exists + if [ -d "$TEMP_PATH" ]; then + rm -rf "$TEMP_PATH" + fi + + # Generate a new application key + php "$LOCAL_PATH"/artisan key:generate --force + + # Clear application cache + php "$LOCAL_PATH"/artisan cache:clear + + # Clear route cache + php "$LOCAL_PATH"/artisan route:clear + + # Clear config cache + php "$LOCAL_PATH"/artisan config:clear + + # Clear compiled views cache + php "$LOCAL_PATH"/artisan view:clear + + # # Optimize the application + # php "$LOCAL_PATH"/artisan optimize + +else + echo "The /var/www/html/public directory exists and contains index.php. Skipping clone and setup." +fi + +# Install FreeRADIUS and FreeRADIUS-MySQL +sudo apt-get install -y freeradius freeradius-mysql freeradius-utils + +# # Specify the path to the FreeRADIUS SQL file +sql_file="/etc/freeradius/3.0/mods-available/sql" + + +# Replace files +sed -i "s/login = .*/login = \"$MYSQL_USER\"/" "$sql_file" +sed -i "s/password = .*/password = \"$MYSQL_PASSWORD\"/" "$sql_file" +# sed -i "s/read_clients = .*/read_clients = yes/" "$sql_file" + +# # Enable SQL module and configure FreeRADIUS to use it +sudo ln -s /etc/freeradius/3.0/mods-available/sql /etc/freeradius/3.0/mods-enabled/ + +# # Enable & Restart FreeRADIUS service +systemctl enable freeradius.service +sudo systemctl restart freeradius.service + +# Install OpenVPN +sudo apt-get update + +# Set the environment variable +export AUTO_INSTALL=y + +# Download the openvpn.sh script +curl -O https://raw.githubusercontent.com/simpleisp/bash/main/openvpn.sh + +# Make the openvpn.sh script executable +chmod +x openvpn.sh + +# Run the openvpn.sh script +./openvpn.sh + +# Add an OpenVPN configuration file if needed, e.g.: +# sudo cp /path/to/your/openvpn/config.ovpn /etc/openvpn/ + +# Enable and start OpenVPN service +sudo systemctl enable openvpn +sudo systemctl start openvpn +sudo apt install -y easy-rsa + +# Set permissions for /etc/openvpn +sudo chmod -R 777 /etc/openvpn +sudo chmod -R 777 /etc/openvpn/easy-rsa +sudo ufw allow 1194/tcp + +# Install Php imap +apt install -y php-imap + +# Supervisor Installation + +# Step 1: Install Supervisor +sudo apt-get install -y supervisor + +# Step 2: Configure Supervisor +sudo bash -c "cat > /etc/supervisor/conf.d/queue-worker.conf << EOL +[program:queue-worker] +process_name=%(program_name)s_%(process_num)02d +command=php /var/www/html/artisan queue:work --tries=3 +autostart=true +autorestart=true +stopasgroup=true +killasgroup=true +user=www-data +numprocs=5 +redirect_stderr=true +stdout_logfile=/var/www/html/storage/logs/queue.log +stopwaitsecs=3600 +EOL" + +# Step 3: Start Supervisor +sudo systemctl start supervisor +sudo supervisorctl reread +sudo supervisorctl update +sudo supervisorctl start queue-worker:* + +# Update sudoers file to allow www-data user to restart and check the status of OpenVPN without a password +sudo bash -c "cat >> /etc/sudoers << EOL +www-data ALL=NOPASSWD: /bin/systemctl start openvpn +www-data ALL=NOPASSWD: /bin/systemctl stop openvpn +www-data ALL=NOPASSWD: /bin/systemctl restart openvpn +www-data ALL=NOPASSWD: /bin/systemctl status openvpn +www-data ALL=NOPASSWD: /bin/systemctl reload openvpn +www-data ALL=NOPASSWD: /bin/systemctl enable openvpn +www-data ALL=NOPASSWD: /bin/systemctl disable openvpn +www-data ALL=NOPASSWD: /bin/systemctl start freeradius +www-data ALL=NOPASSWD: /bin/systemctl stop freeradius +www-data ALL=NOPASSWD: /bin/systemctl restart freeradius +www-data ALL=NOPASSWD: /bin/systemctl status freeradius +www-data ALL=NOPASSWD: /bin/systemctl reload freeradius +www-data ALL=NOPASSWD: /bin/systemctl enable freeradius +www-data ALL=NOPASSWD: /bin/systemctl disable freeradius +www-data ALL=NOPASSWD: /bin/supervisorctl stop all +www-data ALL=NOPASSWD: /bin/supervisorctl reread +www-data ALL=NOPASSWD: /bin/supervisorctl update +www-data ALL=NOPASSWD: /bin/supervisorctl start all +www-data ALL=NOPASSWD: /bin/supervisorctl restart all +www-data ALL=NOPASSWD: /bin/supervisorctl status +www-data ALL=NOPASSWD: /bin/systemctl restart supervisor +www-data ALL=NOPASSWD: /bin/systemctl status ssh +www-data ALL=NOPASSWD: /var/www/html/sh/set_permissions.sh +www-data ALL=NOPASSWD: /var/www/html/sh/restart-services.sh +EOL" + +# Set permissions +sudo chown -R www-data:www-data /var/www +sudo chown -R www-data:www-data /var/www/html +sudo chmod -R 777 /var/www/html/storage +sudo chmod -R 777 /var/www/html/bootstrap/cache +sudo chown -R www-data:www-data /var/www/html/bootstrap/cache +sudo timedatectl set-timezone Africa/Nairobi +sudo ufw allow 1812:1813/udp + +# Make script executable +chmod +x /var/www/html/sh/set_permissions.sh +chmod +x /var/www/html/sh/restart-services.sh + +# Install cron +# Write cron job entry to a temporary file +echo "* * * * * php /var/www/html/artisan schedule:run >> /dev/null 2>&1" > cronjob + +# Install the cron job from the temporary file +crontab cronjob + +# Clean up the temporary file +rm cronjob + +# Step 5: Prompt user for email address and install Certbot +read -p "Enter your email address for certificate management: " email_address + +# Install Certbot +sudo apt install python3-certbot-nginx -y + + +# Run Certbot for the given domain +sudo certbot --nginx -d $domain_name --agree-tos --email $email_address --no-eff-email --non-interactive + +# Install Composer dependencies (initial setup) without human interaction +cd "$LOCAL_PATH" +composer install --no-interaction + + + +echo "MariaDB setup completed" +echo "ssl certificate issued" +echo "FreeRADIUS setup completed" +# Display the message to copy the details +echo "" +echo "**********************************************************" +echo "IMPORTANT: Please copy these details. You will need them to continue to the next step." +echo "**********************************************************" +echo "" +echo "Database name: radius" +echo "Generated MySQL username: ${MYSQL_USER}" +echo "Generated MySQL password: ${MYSQL_PASSWORD}" +echo "" +echo "**********************************************************" +echo "Installation completed." +echo "Please access the following link to finalize the setup:" +echo "https://$domain_name/install" +echo "**********************************************************" +echo "" diff --git a/openvpn.sh b/openvpn.sh index 44c04bf..e3cd01c 100644 --- a/openvpn.sh +++ b/openvpn.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/bash # shellcheck disable=SC1091,SC2164,SC2034,SC1072,SC1073,SC1009 # Secure OpenVPN server installer for Debian, Ubuntu, CentOS, Amazon Linux 2, Fedora, Oracle Linux 8, Arch Linux, Rocky Linux and AlmaLinux. @@ -22,7 +22,7 @@ function checkOS() { if [[ $ID == "debian" || $ID == "raspbian" ]]; then if [[ $VERSION_ID -lt 9 ]]; then - echo "⚠️ Your version of Debian is not supported." + echo "⚠️ Your version of Debian is not supported." echo "" echo "However, if you're using Debian >= 9 or unstable/testing then you can continue, at your own risk." echo "" @@ -37,7 +37,7 @@ function checkOS() { OS="ubuntu" MAJOR_UBUNTU_VERSION=$(echo "$VERSION_ID" | cut -d '.' -f1) if [[ $MAJOR_UBUNTU_VERSION -lt 16 ]]; then - echo "⚠️ Your version of Ubuntu is not supported." + echo "⚠️ Your version of Ubuntu is not supported." echo "" echo "However, if you're using Ubuntu >= 16.04 or beta, then you can continue, at your own risk." echo "" @@ -57,7 +57,7 @@ function checkOS() { if [[ $ID == "centos" || $ID == "rocky" || $ID == "almalinux" ]]; then OS="centos" if [[ $VERSION_ID -lt 7 ]]; then - echo "⚠️ Your version of CentOS is not supported." + echo "⚠️ Your version of CentOS is not supported." echo "" echo "The script only support CentOS 7 and CentOS 8." echo "" @@ -76,7 +76,7 @@ function checkOS() { if [[ $ID == "amzn" ]]; then OS="amzn" if [[ $VERSION_ID != "2" ]]; then - echo "⚠️ Your version of Amazon Linux is not supported." + echo "⚠️ Your version of Amazon Linux is not supported." echo "" echo "The script only support Amazon Linux 2." echo "" @@ -237,7 +237,7 @@ function installQuestions() { if [[ $APPROVE_IP =~ n ]]; then read -rp "IP address: " -e -i "$IP" IP fi - # If $IP is a private IP address, the server must be behind NAT + # If $IP is a private IP address, the server must be behind NAT if echo "$IP" | grep -qE '^(10\.|172\.1[6789]\.|172\.2[0-9]\.|172\.3[01]\.|192\.168)'; then echo "" echo "It seems this server is behind NAT. What is its public IPv4 address or hostname?" @@ -1089,7 +1089,7 @@ function newClient() { ./easyrsa --batch build-client-full "$CLIENT" nopass ;; 2) - echo "⚠️ You will be asked for the client password below ⚠️" + echo "⚠️ You will be asked for the client password below ⚠️" ./easyrsa --batch build-client-full "$CLIENT" ;; esac diff --git a/ports.sh b/ports.sh index 380fae2..86b78fd 100644 --- a/ports.sh +++ b/ports.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/bash set -e @@ -55,7 +55,7 @@ sed -i "/^\*filter/i \ # Masquerade Winbox return traffic\n\ -A POSTROUTING -p tcp -d ${SUBNET} --dport 8291 -j MASQUERADE\n\ \n\ -# DNAT rules (.2 → .10)\n\ +# DNAT rules (.2 → .10)\n\ -A PREROUTING -p tcp --dport 2002 -j DNAT --to-destination ${BASE_NET}.2:8291\n\ -A PREROUTING -p tcp --dport 2003 -j DNAT --to-destination ${BASE_NET}.3:8291\n\ -A PREROUTING -p tcp --dport 2004 -j DNAT --to-destination ${BASE_NET}.4:8291\n\ @@ -89,4 +89,4 @@ done # ----------------------------- ufw reload >/dev/null -echo "✔ UFW NAT + forwarding configured successfully (.2 → .10)" +echo "✔ UFW NAT + forwarding configured successfully (.2 → .10)" diff --git a/setup.sh b/setup.sh index 81e42b2..3e0a8cf 100644 --- a/setup.sh +++ b/setup.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/bash # Install the application sudo apt-get update diff --git a/template_generation.sh b/template_generation.sh index 1f62a41..6593254 100644 --- a/template_generation.sh +++ b/template_generation.sh @@ -1,11 +1,11 @@ -qm create 9013 --name ubuntu2604resolute --cores 2 --memory 2048 --net0 virtio,bridge=vmbr1,tag=20 --scsihw virtio-scsi-pci - -qm set 9013 --scsi0 simplux_nbo1:0,import-from=/root/resolute-server-cloudimg-amd64.img - -qm set 9013 --ide2 simplux_nbo1:cloudinit - -qm set 9013 --boot order=scsi0 - -qm set 9013 --serial0 socket --vga serial0 - -qm template 9013 +qm create 9013 --name ubuntu2604resolute --cores 2 --memory 2048 --net0 virtio,bridge=vmbr1,tag=20 --scsihw virtio-scsi-pci + +qm set 9013 --scsi0 simplux_nbo1:0,import-from=/root/resolute-server-cloudimg-amd64.img + +qm set 9013 --ide2 simplux_nbo1:cloudinit + +qm set 9013 --boot order=scsi0 + +qm set 9013 --serial0 socket --vga serial0 + +qm template 9013 diff --git a/transfer_tmpl.sh b/transfer_tmpl.sh index 4adc190..f2748e5 100644 --- a/transfer_tmpl.sh +++ b/transfer_tmpl.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/bash # Check for jq and install if missing if ! command -v jq &> /dev/null; then diff --git a/ubuntu_simpleisp.sh b/ubuntu_simpleisp.sh index 91aa1d6..54811e1 100644 --- a/ubuntu_simpleisp.sh +++ b/ubuntu_simpleisp.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/bash # Setup logging and error handling INSTALL_LOG="/root/install.txt" @@ -11,20 +11,20 @@ PHP_VERSION="7.4" # Logging functions log_info() { - echo "ℹ️ INFO: $1" | tee -a "$INSTALL_LOG" + echo "ℹ️ INFO: $1" | tee -a "$INSTALL_LOG" } log_success() { - echo "✅ SUCCESS: $1" | tee -a "$INSTALL_LOG" + echo "✅ SUCCESS: $1" | tee -a "$INSTALL_LOG" } log_error() { - echo "❌ ERROR: $1" | tee -a "$INSTALL_LOG" + echo "❌ ERROR: $1" | tee -a "$INSTALL_LOG" } log_step() { STEP_COUNT=$((STEP_COUNT + 1)) - echo "👉 STEP $STEP_COUNT: $1" | tee -a "$INSTALL_LOG" + echo "👉 STEP $STEP_COUNT: $1" | tee -a "$INSTALL_LOG" } handle_error() { diff --git a/ubuntu_simplespot.sh b/ubuntu_simplespot.sh index ce73d8c..f3821ea 100644 --- a/ubuntu_simplespot.sh +++ b/ubuntu_simplespot.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/bash # Setup logging and error handling INSTALL_LOG="/root/install.txt" @@ -15,20 +15,20 @@ PHP_VERSION="8.2" # Logging functions log_info() { - echo "ℹ️ INFO: $1" | tee -a "$INSTALL_LOG" + echo "ℹ️ INFO: $1" | tee -a "$INSTALL_LOG" } log_success() { - echo "✅ SUCCESS: $1" | tee -a "$INSTALL_LOG" + echo "✅ SUCCESS: $1" | tee -a "$INSTALL_LOG" } log_error() { - echo "❌ ERROR: $1" | tee -a "$INSTALL_LOG" + echo "❌ ERROR: $1" | tee -a "$INSTALL_LOG" } log_step() { STEP_COUNT=$((STEP_COUNT + 1)) - echo "👉 STEP $STEP_COUNT: $1" | tee -a "$INSTALL_LOG" + echo "👉 STEP $STEP_COUNT: $1" | tee -a "$INSTALL_LOG" } handle_error() { diff --git a/universal.sh b/universal.sh index c0eae72..ce02185 100644 --- a/universal.sh +++ b/universal.sh @@ -1,6 +1,6 @@ -#!/usr/bin/env bash +#!/bin/bash # --------------------------------------------------------------------------------- -# universal2_replaced_complete.sh — Universal autotune & in-place PHP-FPM edits +# universal2_replaced_complete.sh — Universal autotune & in-place PHP-FPM edits # - Backups, MariaDB tuning fragment, index ensures, FreeRADIUS tuning # - PHP-FPM: REPLACE existing pm.* and slowlog/status/catch directives IN-PLACE # - Removes any existing AUTOTUNE block before edits @@ -203,14 +203,14 @@ remove_old() { } log "========================" -log " Step 3: Backup retention — keep last ${KEEP}" +log " Step 3: Backup retention — keep last ${KEEP}" log "------------------------" remove_old "${DB_BACKUP_DIR}" remove_old "${CONF_BACKUP_DIR}" log "" # ---------------------------------------------------------------------- -# Step 4: Schema adjustments — indexes and safe column modifications +# Step 4: Schema adjustments — indexes and safe column modifications # ---------------------------------------------------------------------- log "========================" log " Step 4: Ensure required indexes & column sizes on radius/vouchers" @@ -702,7 +702,7 @@ else log "[DRY] would replace ${key} = ${val} in ${file}" fi else - # No existing line — insert after anchor (anchor is anchor key, e.g., 'pm' for 'pm = dynamic') + # No existing line — insert after anchor (anchor is anchor key, e.g., 'pm' for 'pm = dynamic') if [ -n "$anchor" ] && grep -qE "^[[:space:]]*${anchor}[[:space:]]*=" "$file"; then if [ "$DRY_RUN" -eq 0 ]; then awk -v a="$anchor" -v newline="${key} = ${val}" '{ @@ -768,7 +768,7 @@ else # Write suggestion report cat > "$PHPFPM_SUGGESTION_FILE" < Date: Wed, 17 Jun 2026 14:56:45 +0300 Subject: [PATCH 17/45] fixed mojibake --- ubuntu_simpleisp.sh | 10 +++++----- ubuntu_simplespot.sh | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/ubuntu_simpleisp.sh b/ubuntu_simpleisp.sh index 54811e1..91aa1d6 100644 --- a/ubuntu_simpleisp.sh +++ b/ubuntu_simpleisp.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/bash # Setup logging and error handling INSTALL_LOG="/root/install.txt" @@ -11,20 +11,20 @@ PHP_VERSION="7.4" # Logging functions log_info() { - echo "ℹ️ INFO: $1" | tee -a "$INSTALL_LOG" + echo "ℹ️ INFO: $1" | tee -a "$INSTALL_LOG" } log_success() { - echo "✅ SUCCESS: $1" | tee -a "$INSTALL_LOG" + echo "✅ SUCCESS: $1" | tee -a "$INSTALL_LOG" } log_error() { - echo "❌ ERROR: $1" | tee -a "$INSTALL_LOG" + echo "❌ ERROR: $1" | tee -a "$INSTALL_LOG" } log_step() { STEP_COUNT=$((STEP_COUNT + 1)) - echo "👉 STEP $STEP_COUNT: $1" | tee -a "$INSTALL_LOG" + echo "👉 STEP $STEP_COUNT: $1" | tee -a "$INSTALL_LOG" } handle_error() { diff --git a/ubuntu_simplespot.sh b/ubuntu_simplespot.sh index f3821ea..ce73d8c 100644 --- a/ubuntu_simplespot.sh +++ b/ubuntu_simplespot.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/bash # Setup logging and error handling INSTALL_LOG="/root/install.txt" @@ -15,20 +15,20 @@ PHP_VERSION="8.2" # Logging functions log_info() { - echo "ℹ️ INFO: $1" | tee -a "$INSTALL_LOG" + echo "ℹ️ INFO: $1" | tee -a "$INSTALL_LOG" } log_success() { - echo "✅ SUCCESS: $1" | tee -a "$INSTALL_LOG" + echo "✅ SUCCESS: $1" | tee -a "$INSTALL_LOG" } log_error() { - echo "❌ ERROR: $1" | tee -a "$INSTALL_LOG" + echo "❌ ERROR: $1" | tee -a "$INSTALL_LOG" } log_step() { STEP_COUNT=$((STEP_COUNT + 1)) - echo "👉 STEP $STEP_COUNT: $1" | tee -a "$INSTALL_LOG" + echo "👉 STEP $STEP_COUNT: $1" | tee -a "$INSTALL_LOG" } handle_error() { From 96edae1695022c714b32ab33eb1c10d5970ee89d Mon Sep 17 00:00:00 2001 From: Barnabie Opiyo Date: Wed, 15 Jul 2026 13:09:28 +0300 Subject: [PATCH 18/45] Integrate universal.sh v3, db_cleanup.sh and ovpn_fix.sh into the installers - Rename: optimized simpleisp.sh/simplespot.sh take over the ubuntu_simpleisp.sh/ubuntu_simplespot.sh install entry-point names; previous ubuntu_ scripts kept as *_old.sh - Installers now download universal.sh, db_cleanup.sh, ovpn_fix.sh from this repo into /var/www/html/sh, schedule universal.sh daily at 3 AM and on @reboot (delayed 120s), db_cleanup.sh at 04:30, run universal.sh once for initial tuning and ovpn_fix.sh once as sandbox verification - Remove the embedded legacy update_memory_config.sh (restart-everything tuning) from the installers entirely - universal.sh: remove legacy update_memory_config.sh file + cron on servers that still have it; prune side-by-side .bak.TIMESTAMP config backups to KEEP=3 so daily/reboot runs cannot grow them unbounded; strip BOM and mojibake - Installers: strip BOM breaking the shebang, define missing log_warning, backport COMPOSER_ALLOW_SUPERUSER/--no-security-blocking composer fix Co-Authored-By: Claude Fable 5 --- db_cleanup.sh | 425 +++++-- ovpn_fix.sh | 262 +++++ ubuntu_simpleisp.sh | 1169 +++++++------------- simpleisp.sh => ubuntu_simpleisp_old.sh | 1227 +++++++++++++-------- ubuntu_simplespot.sh | 722 +++--------- simplespot.sh => ubuntu_simplespot_old.sh | 768 +++++++++---- universal.sh | 392 ++++--- 7 files changed, 2709 insertions(+), 2256 deletions(-) create mode 100644 ovpn_fix.sh rename simpleisp.sh => ubuntu_simpleisp_old.sh (56%) rename simplespot.sh => ubuntu_simplespot_old.sh (64%) diff --git a/db_cleanup.sh b/db_cleanup.sh index 5f8e547..4d07b2d 100644 --- a/db_cleanup.sh +++ b/db_cleanup.sh @@ -1,119 +1,356 @@ #!/bin/bash +# ========================================================================= +# db_cleanup.sh (v3) - RADIUS / hotspot database retention & maintenance +# +# - INTERACTIVE: when run from a terminal it asks, per category, how old +# records must be before deletion, in shorthand: 12h, 2d, 1w, 3m +# (hours/days/weeks/months). Enter keeps the default, 's' skips the +# category. A summary + confirmation is shown before anything deletes. +# - NON-INTERACTIVE: without a terminal (cron) or with --auto it runs +# the defaults below, exactly like before. The 04:30 cron needs no flag. +# - Batched deletes, real error handling (exits non-zero on SQL errors), +# zombie radacct closer, failed_jobs purge, ANALYZE + conditional +# OPTIMIZE, flock guard, --dry-run. +# +# Usage: db_cleanup.sh [--dry-run] [--auto] [--ask] +# --dry-run : show what would be deleted, change nothing +# --auto : never prompt, use the defaults below +# --ask : force prompts even when stdin is not a terminal +# ========================================================================= +set -euo pipefail -# Configuration +# ----------------------------- Configuration ----------------------------- DB_NAME="radius" -DB_USER="root" -MYSQL_BIN=$(which mysql) +DB_USER="root" # uses unix_socket auth -> run as root +MYSQL_BIN=$(command -v mysql) LOG_FILE="/var/log/radius_db_cleanup.log" +LOCK_FILE="/var/lock/db_cleanup.lock" -# Ensure log file exists and is writable -touch "$LOG_FILE" 2>/dev/null +BATCH_LIMIT=10000 # rows per DELETE/UPDATE batch +BATCH_SLEEP=0.2 # pause between batches +OPTIMIZE_THRESHOLD_MB=256 # OPTIMIZE when data_free exceeds this +LOG_MAX_BYTES=5242880 # rotate own log beyond 5 MB + +# Default retention ages (shorthand: h=hours, d=days, w=weeks, m=months) +DEF_RADIUS="1w" # closed radacct sessions + radpostauth log +DEF_SESSIONS="1w" # captive portal sessions (expires_at) +DEF_VOUCHERS="2d" # vouchers already marked expired +DEF_PAYMENTS="6m" # hotspot_payments + mpesa_stks ledgers +DEF_FAILED="2d" # Laravel failed_jobs (poison closure adds ~5K rows/day) + +ZOMBIE_AFTER="1 DAY" # open radacct sessions silent this long + # get closed as Stale-Session (not prompted) + +# radcheck/radreply rows whose voucher no longer exists are still-working +# login credentials (freeloader risk). Swept by default; 's' at the prompt +# skips. Only numeric voucher-shaped usernames are touched and never ones +# with an open session, so hand-created static users are always safe. + +# ----------------------------- CLI parsing ------------------------------- +DRY_RUN=0 +FORCE_AUTO=0 +FORCE_ASK=0 +for arg in "$@"; do + case "$arg" in + --dry-run) DRY_RUN=1 ;; + --auto) FORCE_AUTO=1 ;; + --ask) FORCE_ASK=1 ;; + *) echo "Usage: $0 [--dry-run] [--auto] [--ask]" >&2; exit 1 ;; + esac +done + +INTERACTIVE=0 +if [ "$FORCE_ASK" -eq 1 ]; then + INTERACTIVE=1 +elif [ "$FORCE_AUTO" -eq 0 ] && [ -t 0 ]; then + INTERACTIVE=1 +fi + +# ----------------------------- Age parsing ------------------------------- +# "12h" -> "12 HOUR", "2d" -> "2 DAY", "1w" -> "1 WEEK", "3m" -> "3 MONTH" +parse_age() { + local in="$1" + if [[ "$in" =~ ^([0-9]+)([hdwm])$ ]]; then + local n="${BASH_REMATCH[1]}" u="${BASH_REMATCH[2]}" + [ "$n" -eq 0 ] && return 1 + case "$u" in + h) echo "$n HOUR" ;; + d) echo "$n DAY" ;; + w) echo "$n WEEK" ;; + m) echo "$n MONTH" ;; + esac + return 0 + fi + return 1 +} + +# Prompt for one category. Echoes "N UNIT" or "SKIP". +ask_age() { + local label="$1" def="$2" input parsed + while true; do + printf " %-38s [%s] (e.g. 12h/2d/1w/3m, s=skip): " "$label" "$def" > /dev/tty + IFS= read -r input < /dev/tty || input="" + input="${input,,}" + if [ -z "$input" ]; then parse_age "$def"; return; fi + if [ "$input" = "s" ]; then echo "SKIP"; return; fi + if parsed=$(parse_age "$input"); then echo "$parsed"; return; fi + echo " invalid - use a number + h/d/w/m, like 12h, 2d, 1w, 3m" > /dev/tty + done +} + +# ----------------------------- Preflight --------------------------------- +if [ "$(id -u)" -ne 0 ]; then + echo "ERROR: must run as root (MySQL '$DB_USER' uses unix_socket auth)" >&2 + exit 1 +fi + +if [ "$INTERACTIVE" -eq 1 ] && [ ! -e /dev/tty ]; then + echo "ERROR: --ask requires a terminal (/dev/tty unavailable)" >&2 + exit 1 +fi + +exec 9>"$LOCK_FILE" +if ! flock -n 9; then + echo "ERROR: another db_cleanup run is already in progress" >&2 + exit 1 +fi + +# Rotate our own log so it cannot grow unbounded (keep one generation) +if [ -f "$LOG_FILE" ] && [ "$(stat -c%s "$LOG_FILE" 2>/dev/null || echo 0)" -gt "$LOG_MAX_BYTES" ]; then + mv -f "$LOG_FILE" "${LOG_FILE}.old" +fi +touch "$LOG_FILE" -# Logging function to guarantee clean output to screen and file log_message() { local message="$1" - echo "$message" - echo "$message" >> "$LOG_FILE" + echo "$message" + echo "$message" >> "$LOG_FILE" } +ERRF=$(mktemp) +trap 'rm -f "$ERRF"' EXIT + +if ! "$MYSQL_BIN" -u "$DB_USER" "$DB_NAME" -N -e "SELECT 1" >/dev/null 2>"$ERRF"; then + echo "ERROR: cannot connect to MySQL as '$DB_USER': $(tail -n1 "$ERRF")" >&2 + exit 1 +fi + +# ----------------------------- Choose ages ------------------------------- +if [ "$INTERACTIVE" -eq 1 ]; then + echo "Delete records OLDER than... (Enter = default shown in brackets)" > /dev/tty + KEEP_RADIUS=$(ask_age "RADIUS logs (radacct + radpostauth)" "$DEF_RADIUS") + KEEP_SESSIONS=$(ask_age "Portal sessions (hotspot_sessions)" "$DEF_SESSIONS") + KEEP_VOUCHERS=$(ask_age "Expired vouchers" "$DEF_VOUCHERS") + KEEP_PAYMENTS=$(ask_age "Payment ledgers (payments + STKs)" "$DEF_PAYMENTS") + KEEP_FAILED=$(ask_age "Laravel failed_jobs" "$DEF_FAILED") + printf " %-38s [Y/s] (auth leftovers of deleted vouchers): " "Orphaned radcheck/radreply rows" > /dev/tty + IFS= read -r oans < /dev/tty || oans="" + if [ "${oans,,}" = "s" ]; then CLEAN_ORPHANS="SKIP"; else CLEAN_ORPHANS="n/a"; fi + echo > /dev/tty + ORPH_SHOW="clean"; [ "$CLEAN_ORPHANS" = "SKIP" ] && ORPH_SHOW="skip" + echo " Chosen: radius=[$KEEP_RADIUS] sessions=[$KEEP_SESSIONS] vouchers=[$KEEP_VOUCHERS] payments=[$KEEP_PAYMENTS] failed_jobs=[$KEEP_FAILED] orphans=[$ORPH_SHOW]" > /dev/tty + [ "$DRY_RUN" -eq 1 ] && echo " (dry-run: nothing will actually be deleted)" > /dev/tty + printf " Proceed? [y/N]: " > /dev/tty + IFS= read -r confirm < /dev/tty || confirm="" + if [ "${confirm,,}" != "y" ]; then + echo "Aborted - nothing deleted." > /dev/tty + exit 0 + fi +else + KEEP_RADIUS=$(parse_age "$DEF_RADIUS") + KEEP_SESSIONS=$(parse_age "$DEF_SESSIONS") + KEEP_VOUCHERS=$(parse_age "$DEF_VOUCHERS") + KEEP_PAYMENTS=$(parse_age "$DEF_PAYMENTS") + KEEP_FAILED=$(parse_age "$DEF_FAILED") + CLEAN_ORPHANS="n/a" +fi + log_message "=========================================================" log_message "Starting Database Maintenance Run: $(date)" +[ "$DRY_RUN" -eq 1 ] && log_message "*** DRY-RUN MODE: no rows will be modified ***" +log_message "Ages: radius=$KEEP_RADIUS sessions=$KEEP_SESSIONS vouchers=$KEEP_VOUCHERS payments=$KEEP_PAYMENTS failed=$KEEP_FAILED orphans=$([ "$CLEAN_ORPHANS" = "SKIP" ] && echo skip || echo clean)" log_message "=========================================================" -# Initialize dynamic counter variables for reporting -TOTAL_RADACCT=0 -TOTAL_RADPOSTAUTH=0 -TOTAL_HOTSPOT_SESSIONS=0 -TOTAL_VOUCHERS_EXPIRED=0 -TOTAL_HOTSPOT_PAYMENTS=0 -TOTAL_MPESA_STKS=0 - -# NEW: Unified helper function to delete and count in the exact same session -execute_and_count() { - local query="$1" - local result - - # Run the query and append SELECT ROW_COUNT() in the same transaction - result=$($MYSQL_BIN -u "$DB_USER" "$DB_NAME" -N -e "$query; SELECT ROW_COUNT();" 2>/dev/null) - - # Ensure the result is a valid number, otherwise return 0 - if [[ ! "$result" =~ ^[0-9]+$ ]]; then - echo 0 +# ----------------------------- SQL helpers ------------------------------- +ERRORS=0 +LAST_ERR="" +RESULT=0 + +# Run a modifying statement; print affected-row count, or "ERR" on failure +# (LAST_ERR then holds the MySQL error). Never kills the script by itself. +run_sql_count() { + local query="$1" out + if ! out=$("$MYSQL_BIN" -u "$DB_USER" "$DB_NAME" -N -e "${query}; SELECT ROW_COUNT();" 2>"$ERRF"); then + LAST_ERR=$(tail -n1 "$ERRF" 2>/dev/null || echo "unknown mysql error") + echo "ERR" + return 0 + fi + out=$(printf '%s\n' "$out" | tail -n1) + if [[ "$out" =~ ^-?[0-9]+$ ]]; then + echo "$out" else - echo "$result" + LAST_ERR="unexpected output: $out" + echo "ERR" fi } -# --------------------------------------------------------- -# 1. RADIUS AAA Operational Tables (1-Week Window) -# --------------------------------------------------------- -log_message "Cleaning up core RADIUS operational logs older than 1 week..." - -RADACCT_QUERY="DELETE FROM radacct WHERE acctstoptime IS NOT NULL AND acctstoptime < NOW() - INTERVAL 1 WEEK LIMIT 10000" +# Run a scalar SELECT; print the value, or "ERR" on failure. +run_scalar() { + local query="$1" out + if ! out=$("$MYSQL_BIN" -u "$DB_USER" "$DB_NAME" -N -e "$query" 2>"$ERRF"); then + LAST_ERR=$(tail -n1 "$ERRF" 2>/dev/null || echo "unknown mysql error") + echo "ERR" + return 0 + fi + echo "$out" +} -while true; do - ROWS=$(execute_and_count "$RADACCT_QUERY") - if [ "$ROWS" -le 0 ]; then break; fi - TOTAL_RADACCT=$((TOTAL_RADACCT + ROWS)) - sleep 0.2 -done -log_message " -> radacct processing complete." +# Apply one retention rule in batches until no rows remain. +# $1 label $2 keep-age ("N UNIT" or "SKIP") +# $3 modifying SQL template with __AGE__ placeholder (embeds LIMIT) +# $4 COUNT(*) SQL template with __AGE__ placeholder (for --dry-run) +# Total affected rows land in $RESULT ("skipped" when skipped). +apply_batched() { + local label="$1" age="$2" sql_tpl="$3" count_tpl="$4" sql count_sql rows suffix="" + RESULT=0 -RADPOSTAUTH_QUERY="DELETE FROM radpostauth WHERE authdate < NOW() - INTERVAL 1 WEEK LIMIT 10000" + if [ "$age" = "SKIP" ]; then + RESULT="skipped" + log_message " -> $label: skipped by operator" + return 0 + fi + [ "$age" != "n/a" ] && suffix=" (older than $age)" + sql="${sql_tpl//__AGE__/$age}" + count_sql="${count_tpl//__AGE__/$age}" -while true; do - ROWS=$(execute_and_count "$RADPOSTAUTH_QUERY") - if [ "$ROWS" -le 0 ]; then break; fi - TOTAL_RADPOSTAUTH=$((TOTAL_RADPOSTAUTH + ROWS)) - sleep 0.2 -done -log_message " -> radpostauth processing complete." + if [ "$DRY_RUN" -eq 1 ]; then + rows=$(run_scalar "$count_sql") + if [ "$rows" = "ERR" ]; then + log_message " [ERROR] $label: $LAST_ERR" + ERRORS=$((ERRORS + 1)) + return 0 + fi + RESULT="$rows" + log_message " [DRY] $label$suffix: $rows row(s) would be affected" + return 0 + fi + while true; do + rows=$(run_sql_count "$sql") + if [ "$rows" = "ERR" ]; then + log_message " [ERROR] $label: $LAST_ERR (rule aborted after $RESULT rows)" + ERRORS=$((ERRORS + 1)) + return 0 + fi + if [ "$rows" -le 0 ]; then + break + fi + RESULT=$((RESULT + rows)) + sleep "$BATCH_SLEEP" + done + log_message " -> $label$suffix: $RESULT row(s) affected" +} # --------------------------------------------------------- -# 2. Hotspot App Sessions & Depleted Vouchers (2-Day Window) +# 1. Zombie radacct sessions (open, but silent too long) # --------------------------------------------------------- -log_message "Cleaning up captive portal records and depleted vouchers..." +log_message "Closing zombie radacct sessions (no update for > ${ZOMBIE_AFTER})..." +apply_batched "radacct zombies closed" "$ZOMBIE_AFTER" \ + "UPDATE radacct SET acctstoptime = COALESCE(acctupdatetime, acctstarttime), acctterminatecause = 'Stale-Session' WHERE acctstoptime IS NULL AND COALESCE(acctupdatetime, acctstarttime) < NOW() - INTERVAL __AGE__ LIMIT $BATCH_LIMIT" \ + "SELECT COUNT(*) FROM radacct WHERE acctstoptime IS NULL AND COALESCE(acctupdatetime, acctstarttime) < NOW() - INTERVAL __AGE__" +TOTAL_ZOMBIES=$RESULT -HOTSPOT_SESSION_QUERY="DELETE FROM hotspot_sessions WHERE expires_at < NOW() - INTERVAL 1 WEEK LIMIT 5000" +# --------------------------------------------------------- +# 2. RADIUS AAA operational tables +# --------------------------------------------------------- +log_message "Cleaning core RADIUS operational logs..." +apply_batched "radacct (closed sessions)" "$KEEP_RADIUS" \ + "DELETE FROM radacct WHERE acctstoptime IS NOT NULL AND acctstoptime < NOW() - INTERVAL __AGE__ LIMIT $BATCH_LIMIT" \ + "SELECT COUNT(*) FROM radacct WHERE acctstoptime IS NOT NULL AND acctstoptime < NOW() - INTERVAL __AGE__" +TOTAL_RADACCT=$RESULT -while true; do - ROWS=$(execute_and_count "$HOTSPOT_SESSION_QUERY") - if [ "$ROWS" -le 0 ]; then break; fi - TOTAL_HOTSPOT_SESSIONS=$((TOTAL_HOTSPOT_SESSIONS + ROWS)) - sleep 0.1 -done -log_message " -> hotspot_sessions processing complete." +apply_batched "radpostauth" "$KEEP_RADIUS" \ + "DELETE FROM radpostauth WHERE authdate < NOW() - INTERVAL __AGE__ LIMIT $BATCH_LIMIT" \ + "SELECT COUNT(*) FROM radpostauth WHERE authdate < NOW() - INTERVAL __AGE__" +TOTAL_RADPOSTAUTH=$RESULT -VOUCHER_STATUS_CLEANUP="DELETE FROM vouchers WHERE status = 'expired' AND expiration_time < NOW() - INTERVAL 2 DAY" -ROWS=$(execute_and_count "$VOUCHER_STATUS_CLEANUP") -TOTAL_VOUCHERS_EXPIRED=$((TOTAL_VOUCHERS_EXPIRED + ROWS)) -log_message " -> Vouchers expired/depleted for more than 2 days cleared." +# --------------------------------------------------------- +# 3. Captive portal sessions & expired vouchers +# --------------------------------------------------------- +log_message "Cleaning captive portal records and expired vouchers..." +apply_batched "hotspot_sessions" "$KEEP_SESSIONS" \ + "DELETE FROM hotspot_sessions WHERE expires_at < NOW() - INTERVAL __AGE__ LIMIT $BATCH_LIMIT" \ + "SELECT COUNT(*) FROM hotspot_sessions WHERE expires_at < NOW() - INTERVAL __AGE__" +TOTAL_HOTSPOT_SESSIONS=$RESULT +apply_batched "vouchers (expired)" "$KEEP_VOUCHERS" \ + "DELETE FROM vouchers WHERE status = 'expired' AND expiration_time < NOW() - INTERVAL __AGE__ LIMIT $BATCH_LIMIT" \ + "SELECT COUNT(*) FROM vouchers WHERE status = 'expired' AND expiration_time < NOW() - INTERVAL __AGE__" +TOTAL_VOUCHERS_EXPIRED=$RESULT # --------------------------------------------------------- -# 3. Application Payment Ledgers (6-Month Retention Window) +# 4. Application payment ledgers # --------------------------------------------------------- -log_message "Cleaning up payment transactions older than 6 months..." +log_message "Cleaning payment transactions older than retention..." +apply_batched "hotspot_payments" "$KEEP_PAYMENTS" \ + "DELETE FROM hotspot_payments WHERE created_at < NOW() - INTERVAL __AGE__ LIMIT $BATCH_LIMIT" \ + "SELECT COUNT(*) FROM hotspot_payments WHERE created_at < NOW() - INTERVAL __AGE__" +TOTAL_HOTSPOT_PAYMENTS=$RESULT -PAYMENT_CLEANUP_QUERY="DELETE FROM hotspot_payments WHERE created_at < NOW() - INTERVAL 6 MONTH LIMIT 5000" +apply_batched "mpesa_stks" "$KEEP_PAYMENTS" \ + "DELETE FROM mpesa_stks WHERE created_at < NOW() - INTERVAL __AGE__ LIMIT $BATCH_LIMIT" \ + "SELECT COUNT(*) FROM mpesa_stks WHERE created_at < NOW() - INTERVAL __AGE__" +TOTAL_MPESA_STKS=$RESULT -while true; do - ROWS=$(execute_and_count "$PAYMENT_CLEANUP_QUERY") - if [ "$ROWS" -le 0 ]; then break; fi - TOTAL_HOTSPOT_PAYMENTS=$((TOTAL_HOTSPOT_PAYMENTS + ROWS)) - sleep 0.1 -done -log_message " -> hotspot_payments processing complete." +# --------------------------------------------------------- +# 5. Laravel queue: failed_jobs +# --------------------------------------------------------- +log_message "Cleaning Laravel failed_jobs..." +apply_batched "failed_jobs" "$KEEP_FAILED" \ + "DELETE FROM failed_jobs WHERE failed_at < NOW() - INTERVAL __AGE__ LIMIT $BATCH_LIMIT" \ + "SELECT COUNT(*) FROM failed_jobs WHERE failed_at < NOW() - INTERVAL __AGE__" +TOTAL_FAILED_JOBS=$RESULT -STK_CLEANUP_QUERY="DELETE FROM mpesa_stks WHERE created_at < NOW() - INTERVAL 6 MONTH" -ROWS=$(execute_and_count "$STK_CLEANUP_QUERY") -TOTAL_MPESA_STKS=$((TOTAL_MPESA_STKS + ROWS)) -log_message " -> mpesa_stks processing complete." +# --------------------------------------------------------- +# 5b. Orphaned RADIUS auth rows (voucher deleted, credentials remain) +# --------------------------------------------------------- +log_message "Sweeping orphaned radcheck/radreply rows..." +apply_batched "radcheck orphans" "$CLEAN_ORPHANS" \ + "DELETE FROM radcheck WHERE username REGEXP '^[0-9]{6,10}$' AND NOT EXISTS (SELECT 1 FROM vouchers v WHERE v.code = radcheck.username) AND NOT EXISTS (SELECT 1 FROM radacct r WHERE r.username = radcheck.username AND r.acctstoptime IS NULL) LIMIT $BATCH_LIMIT" \ + "SELECT COUNT(*) FROM radcheck WHERE username REGEXP '^[0-9]{6,10}$' AND NOT EXISTS (SELECT 1 FROM vouchers v WHERE v.code = radcheck.username) AND NOT EXISTS (SELECT 1 FROM radacct r WHERE r.username = radcheck.username AND r.acctstoptime IS NULL)" +TOTAL_RADCHECK_ORPHANS=$RESULT +apply_batched "radreply orphans" "$CLEAN_ORPHANS" \ + "DELETE FROM radreply WHERE username REGEXP '^[0-9]{6,10}$' AND NOT EXISTS (SELECT 1 FROM vouchers v WHERE v.code = radreply.username) AND NOT EXISTS (SELECT 1 FROM radacct r WHERE r.username = radreply.username AND r.acctstoptime IS NULL) LIMIT $BATCH_LIMIT" \ + "SELECT COUNT(*) FROM radreply WHERE username REGEXP '^[0-9]{6,10}$' AND NOT EXISTS (SELECT 1 FROM vouchers v WHERE v.code = radreply.username) AND NOT EXISTS (SELECT 1 FROM radacct r WHERE r.username = radreply.username AND r.acctstoptime IS NULL)" +TOTAL_RADREPLY_ORPHANS=$RESULT # --------------------------------------------------------- -# 4. Final Maintenance Summary Dashboard +# 6. Post-maintenance: refresh stats, reclaim disk when worth it +# --------------------------------------------------------- +if [ "$DRY_RUN" -eq 0 ]; then + log_message "Refreshing table statistics / reclaiming free space..." + for t in radacct radpostauth hotspot_sessions vouchers radcheck radreply hotspot_payments mpesa_stks failed_jobs; do + if ! "$MYSQL_BIN" -u "$DB_USER" "$DB_NAME" -e "ANALYZE TABLE \`$t\`;" >/dev/null 2>"$ERRF"; then + log_message " [WARN] ANALYZE $t failed: $(tail -n1 "$ERRF")" + continue + fi + free_mb=$(run_scalar "SELECT COALESCE(ROUND(data_free/1048576),0) FROM information_schema.tables WHERE table_schema='$DB_NAME' AND table_name='$t'") + if [[ "$free_mb" =~ ^[0-9]+$ ]] && [ "$free_mb" -gt "$OPTIMIZE_THRESHOLD_MB" ]; then + log_message " -> $t carries ${free_mb} MB reclaimable space; running OPTIMIZE..." + if ! "$MYSQL_BIN" -u "$DB_USER" "$DB_NAME" -e "OPTIMIZE TABLE \`$t\`;" >/dev/null 2>"$ERRF"; then + log_message " [WARN] OPTIMIZE $t failed: $(tail -n1 "$ERRF")" + else + log_message " -> $t optimized." + fi + fi + done +else + log_message "[DRY] would ANALYZE tables and OPTIMIZE any with > ${OPTIMIZE_THRESHOLD_MB} MB free space" +fi + +# --------------------------------------------------------- +# 7. Summary # --------------------------------------------------------- log_message "" log_message "=========================================================" @@ -121,22 +358,24 @@ log_message " MAINTENANCE CLEANUP SUMMARY REPORT " log_message "=========================================================" log_message "Execution Timestamp : $(date)" log_message "Target Database : $DB_NAME" +[ "$DRY_RUN" -eq 1 ] && log_message "Mode : DRY-RUN (nothing was modified)" log_message "---------------------------------------------------------" -LINE=$(printf "%-25s | %-15s | %-15s" "Table Name" "Retention Pol" "Records Cleared") -log_message "$LINE" +log_message "$(printf "%-28s | %-12s | %-12s" "Rule" "Older than" "Rows")" log_message "---------------------------------------------------------" -LINE=$(printf "%-25s | %-15s | %-15s" "radacct" "1 Week" "$TOTAL_RADACCT") -log_message "$LINE" -LINE=$(printf "%-25s | %-15s | %-15s" "radpostauth" "1 Week" "$TOTAL_RADPOSTAUTH") -log_message "$LINE" -LINE=$(printf "%-25s | %-15s | %-15s" "hotspot_sessions" "1 Week" "$TOTAL_HOTSPOT_SESSIONS") -log_message "$LINE" -LINE=$(printf "%-25s | %-15s | %-15s" "vouchers (Status=Expired)" "2 Days" "$TOTAL_VOUCHERS_EXPIRED") -log_message "$LINE" -LINE=$(printf "%-25s | %-15s | %-15s" "hotspot_payments" "6 Months" "$TOTAL_HOTSPOT_PAYMENTS") -log_message "$LINE" -LINE=$(printf "%-25s | %-15s | %-15s" "mpesa_stks" "6 Months" "$TOTAL_MPESA_STKS") -log_message "$LINE" +log_message "$(printf "%-28s | %-12s | %-12s" "radacct zombies closed" "$ZOMBIE_AFTER" "$TOTAL_ZOMBIES")" +log_message "$(printf "%-28s | %-12s | %-12s" "radacct" "$KEEP_RADIUS" "$TOTAL_RADACCT")" +log_message "$(printf "%-28s | %-12s | %-12s" "radpostauth" "$KEEP_RADIUS" "$TOTAL_RADPOSTAUTH")" +log_message "$(printf "%-28s | %-12s | %-12s" "hotspot_sessions" "$KEEP_SESSIONS" "$TOTAL_HOTSPOT_SESSIONS")" +log_message "$(printf "%-28s | %-12s | %-12s" "vouchers (expired)" "$KEEP_VOUCHERS" "$TOTAL_VOUCHERS_EXPIRED")" +log_message "$(printf "%-28s | %-12s | %-12s" "hotspot_payments" "$KEEP_PAYMENTS" "$TOTAL_HOTSPOT_PAYMENTS")" +log_message "$(printf "%-28s | %-12s | %-12s" "mpesa_stks" "$KEEP_PAYMENTS" "$TOTAL_MPESA_STKS")" +log_message "$(printf "%-28s | %-12s | %-12s" "failed_jobs" "$KEEP_FAILED" "$TOTAL_FAILED_JOBS")" +log_message "$(printf "%-28s | %-12s | %-12s" "radcheck orphans" "$CLEAN_ORPHANS" "$TOTAL_RADCHECK_ORPHANS")" +log_message "$(printf "%-28s | %-12s | %-12s" "radreply orphans" "$CLEAN_ORPHANS" "$TOTAL_RADREPLY_ORPHANS")" log_message "=========================================================" +if [ "$ERRORS" -gt 0 ]; then + log_message "Database Maintenance completed WITH $ERRORS ERROR(S) - see above." + exit 1 +fi log_message "Database Maintenance Completed Successfully." -log_message "=========================================================" \ No newline at end of file +log_message "=========================================================" diff --git a/ovpn_fix.sh b/ovpn_fix.sh new file mode 100644 index 0000000..f0c8ab1 --- /dev/null +++ b/ovpn_fix.sh @@ -0,0 +1,262 @@ +#!/bin/bash +# --------------------------------------------------------------------------- +# fix-phpfpm-openvpn-sandbox.sh +# +# Problem this fixes: +# Debian/Ubuntu php-fpm systemd units ship with ProtectSystem=full, which +# mounts /etc read-only inside the service's mount namespace. Any panel +# code (SimpleISP OpenVPN setup) that writes /etc/openvpn/server.conf from +# a PHP-FPM worker then fails with: +# file_put_contents(/etc/openvpn/server.conf): +# Failed to open stream: Read-only file system +# The sandbox only engages on service (re)start, so the failure typically +# appears right after a php-fpm restart (e.g. after tuning scripts or +# package upgrades). +# +# What it does: +# 1. Finds every installed phpX.Y-fpm unit (Ubuntu 20.04 / 24.04 / 26.04 +# ship different PHP versions; nothing is hardcoded). +# 2. Also finds any systemd unit running "artisan queue:work|horizon" +# (Laravel queue workers write configs too and are sandboxed +# independently of php-fpm). +# 3. Installs a drop-in override: +# [Service] +# ReadWritePaths=/etc/openvpn +# Drop-ins survive package upgrades; editing the packaged unit does not. +# 4. Reloads systemd, restarts the affected units, and VERIFIES from +# inside each service's mount namespace (nsenter) that /etc/openvpn is +# actually writable now. +# +# Usage: +# sudo ./fix-phpfpm-openvpn-sandbox.sh # apply +# sudo ./fix-phpfpm-openvpn-sandbox.sh --dry-run # preview only +# sudo ./fix-phpfpm-openvpn-sandbox.sh --no-restart # install drop-ins only +# +# Notes: +# * Restarting php-fpm briefly recycles workers; on a busy hotspot portal +# run this in a quiet moment. +# * Idempotent: safe to run repeatedly; existing correct drop-ins are +# left untouched. +# --------------------------------------------------------------------------- +set -euo pipefail + +RW_PATH="/etc/openvpn" +DROPIN_NAME="openvpn-write.conf" +DRY_RUN=0 +DO_RESTART=1 +FAILED=0 + +usage() { + cat <&2; } +err() { echo "[$(date +'%F %T')] [ERROR] $*" >&2; } + +if [ "$(id -u)" -ne 0 ]; then + err "This script must run as root." + exit 1 +fi + +if ! command -v systemctl >/dev/null 2>&1; then + err "systemctl not found; this fix only applies to systemd systems." + exit 1 +fi + +# --------------------------------------------------------------------------- +# Discover target units +# --------------------------------------------------------------------------- +declare -a TARGET_UNITS=() + +# All installed phpX.Y-fpm units (covers 7.4 on 20.04 through whatever +# 26.04 ships), whether currently active or not. +while IFS= read -r unit; do + [ -n "$unit" ] && TARGET_UNITS+=("$unit") +done < <(systemctl list-unit-files --type=service --no-legend --no-pager 2>/dev/null \ + | awk '{print $1}' \ + | grep -E '^php[0-9]+(\.[0-9]+)?-fpm\.service$' || true) + +if [ "${#TARGET_UNITS[@]}" -eq 0 ]; then + warn "No php-fpm units found on this host." +fi + +# Laravel queue workers / Horizon run in their own units with their own +# sandboxing; if they perform the config write, they need the same override. +while IFS= read -r unit; do + [ -n "$unit" ] && TARGET_UNITS+=("$unit") +done < <(systemctl list-units --type=service --no-legend --no-pager 2>/dev/null \ + | awk '{print $1}' \ + | while IFS= read -r u; do + execline=$(systemctl show "$u" -p ExecStart --value 2>/dev/null || true) + case "$execline" in + *"artisan queue:work"*|*"artisan horizon"*) echo "$u" ;; + esac + done || true) + +if [ "${#TARGET_UNITS[@]}" -eq 0 ]; then + err "Nothing to do: no php-fpm or queue-worker units found." + exit 1 +fi + +# De-duplicate +mapfile -t TARGET_UNITS < <(printf '%s\n' "${TARGET_UNITS[@]}" | sort -u) + +log "Ubuntu release: $(. /etc/os-release 2>/dev/null && echo "${PRETTY_NAME:-unknown}")" +log "Target units: ${TARGET_UNITS[*]}" +log "ReadWritePaths to grant: ${RW_PATH}" +log "Dry-run: ${DRY_RUN} Restart: ${DO_RESTART}" +echo + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- +needs_fix() { + # Returns 0 if the unit sandboxes /etc and lacks the RW exception. + local unit="$1" protect rwpaths + protect=$(systemctl show "$unit" -p ProtectSystem --value 2>/dev/null || echo "") + rwpaths=$(systemctl show "$unit" -p ReadWritePaths --value 2>/dev/null || echo "") + case " $rwpaths " in + *" ${RW_PATH} "*) return 1 ;; # already granted + esac + case "$protect" in + full|strict|yes|true) return 0 ;; + *) return 1 ;; # /etc not read-only for this unit + esac +} + +install_dropin() { + local unit="$1" + local dir="/etc/systemd/system/${unit}.d" + local file="${dir}/${DROPIN_NAME}" + + if [ -f "$file" ] && grep -q "^ReadWritePaths=.*${RW_PATH}" "$file"; then + log "[SKIP] ${unit}: drop-in already present at ${file}" + return 0 + fi + + if [ "$DRY_RUN" -eq 1 ]; then + log "[DRY] ${unit}: would write ${file} with ReadWritePaths=${RW_PATH}" + return 0 + fi + + mkdir -p "$dir" + cat > "$file" </dev/null || echo 0) + if [ -z "$pid" ] || [ "$pid" -eq 0 ]; then + warn "${unit}: no main PID (service not running?); cannot verify namespace." + return 1 + fi + if ! command -v nsenter >/dev/null 2>&1; then + warn "nsenter not available; skipping in-namespace verification." + return 0 + fi + testfile="${RW_PATH}/.rwtest.$$" + if nsenter -t "$pid" -m -- /bin/sh -c "touch '${testfile}' && rm -f '${testfile}'" 2>/dev/null; then + log "[VERIFIED] ${unit}: ${RW_PATH} is writable inside the service namespace" + return 0 + else + err "${unit}: ${RW_PATH} is STILL read-only inside the service namespace" + return 1 + fi +} + +# --------------------------------------------------------------------------- +# Apply +# --------------------------------------------------------------------------- +declare -a CHANGED_UNITS=() + +for unit in "${TARGET_UNITS[@]}"; do + protect=$(systemctl show "$unit" -p ProtectSystem --value 2>/dev/null || echo "") + rwpaths=$(systemctl show "$unit" -p ReadWritePaths --value 2>/dev/null || echo "") + log "${unit}: ProtectSystem=${protect:-} ReadWritePaths=${rwpaths:-}" + + if needs_fix "$unit"; then + install_dropin "$unit" + CHANGED_UNITS+=("$unit") + else + # Still ensure a drop-in exists for units already carrying the path + # via our own previous run; otherwise nothing to do. + case " $(systemctl show "$unit" -p ReadWritePaths --value 2>/dev/null) " in + *" ${RW_PATH} "*) log "[SKIP] ${unit}: already has ${RW_PATH} in ReadWritePaths" ;; + *) log "[SKIP] ${unit}: /etc is not sandboxed for this unit (ProtectSystem=${protect:-unset})" ;; + esac + fi +done + +if [ "${#CHANGED_UNITS[@]}" -eq 0 ]; then + log "No changes needed on this host." + exit 0 +fi + +if [ "$DRY_RUN" -eq 1 ]; then + log "[DRY] would run: systemctl daemon-reload && systemctl restart ${CHANGED_UNITS[*]}" + exit 0 +fi + +systemctl daemon-reload +log "[OK] systemd daemon reloaded" + +if [ "$DO_RESTART" -eq 0 ]; then + warn "Restart skipped (--no-restart); the override takes effect on each unit's next restart." + exit 0 +fi + +for unit in "${CHANGED_UNITS[@]}"; do + log "[RUN] restarting ${unit}" + if systemctl restart "$unit"; then + if systemctl is-active --quiet "$unit"; then + log "[OK] ${unit} restarted and active" + else + err "${unit} restarted but is not active; check: journalctl -u ${unit} -n 100" + FAILED=1 + continue + fi + else + err "failed to restart ${unit}" + FAILED=1 + continue + fi + verify_unit "$unit" || FAILED=1 +done + +# For queue workers managed by supervisor rather than systemd, remind the +# operator: supervisor does not sandbox, but workers cache code/config. +if command -v supervisorctl >/dev/null 2>&1; then + warn "supervisord detected: if Laravel queue workers run under supervisor," + warn "run 'php artisan queue:restart' so workers pick up fresh state." +fi + +echo +if [ "$FAILED" -eq 0 ]; then + log "All done. PHP-FPM (and any queue-worker units) can now write ${RW_PATH}." +else + err "Completed with errors on one or more units - review output above." + exit 1 +fi \ No newline at end of file diff --git a/ubuntu_simpleisp.sh b/ubuntu_simpleisp.sh index 91aa1d6..bf40ab5 100644 --- a/ubuntu_simpleisp.sh +++ b/ubuntu_simpleisp.sh @@ -5,27 +5,20 @@ INSTALL_LOG="/root/install.txt" STEP_COUNT=0 COMPLETED_STEPS=() +# Get server hostname and set email +DOMAIN=$(hostname -f) +EMAIL_ADDRESS="simpluxsolutions@gmail.com" + # Configuration variables - ONLY DIFFERENCES BETWEEN SCRIPTS GITHUB_REPO_URL="https://github.com/simpleisp/radius.git" PHP_VERSION="7.4" # Logging functions -log_info() { - echo "ℹ️ INFO: $1" | tee -a "$INSTALL_LOG" -} - -log_success() { - echo "✅ SUCCESS: $1" | tee -a "$INSTALL_LOG" -} - -log_error() { - echo "❌ ERROR: $1" | tee -a "$INSTALL_LOG" -} - -log_step() { - STEP_COUNT=$((STEP_COUNT + 1)) - echo "👉 STEP $STEP_COUNT: $1" | tee -a "$INSTALL_LOG" -} +log_info() { echo "ℹ️ INFO: $1" | tee -a "$INSTALL_LOG"; } +log_success() { echo "✅ SUCCESS: $1" | tee -a "$INSTALL_LOG"; } +log_error() { echo "❌ ERROR: $1" | tee -a "$INSTALL_LOG"; } +log_warning() { echo "⚠️ WARNING: $1" | tee -a "$INSTALL_LOG"; } +log_step() { STEP_COUNT=$((STEP_COUNT + 1)); echo "👉 STEP $STEP_COUNT: $1" | tee -a "$INSTALL_LOG"; } handle_error() { log_error "$1" @@ -61,7 +54,7 @@ if [ -f "$CLEANUP_MARKER" ]; then log_info "Detected previous cleanup ($(cat $CLEANUP_MARKER))" log_info "Forcing reinstallation of critical directories and files" REINSTALL=true - + # Remove the marker file after handling it rm -f "$CLEANUP_MARKER" log_success "Cleanup marker processed and removed" @@ -69,13 +62,9 @@ fi # Ensure script runs as root log_step "Checking root privileges" -if [ "$EUID" -ne 0 ]; then - handle_error "Please run as root" -fi +if [ "$EUID" -ne 0 ]; then handle_error "Please run as root"; fi COMPLETED_STEPS+=("Root check passed") -# Set environment variable to avoid interactive prompts -export DEBIAN_FRONTEND=noninteractive # Get Ubuntu version log_step "Detecting Ubuntu version" UBUNTU_VERSION=$(lsb_release -cs) || handle_error "Failed to detect Ubuntu version" @@ -98,7 +87,7 @@ if [ "$UBUNTU_VERSION" = "noble" ]; then gpg --dry-run --quiet --import --import-options import-show ${gpgKeyPath} # Create the sources file for PHP repository - cat > /etc/apt/sources.list.d/ondrej-ubuntu-php-noble.sources << "EOL" + cat > /etc/apt/sources.list.d/ondrej-ubuntu-php-noble.sources << 'EOL' Types: deb URIs: https://ppa.launchpadcontent.net/ondrej/php/ubuntu/ Suites: noble @@ -122,46 +111,18 @@ printf 'Package: /freeradius/\nPin: origin "packages.networkradius.com"\nPin-Pri # Add NetworkRADIUS repository based on Ubuntu version case $UBUNTU_VERSION in - "noble") - REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/noble noble main" - ;; - "jammy") - REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/jammy jammy main" - ;; - "focal") - REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/focal focal main" - ;; - *) - handle_error "Unsupported Ubuntu version: $UBUNTU_VERSION" - ;; + "noble") REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/noble noble main" ;; + "jammy") REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/jammy jammy main" ;; + "focal") REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/focal focal main" ;; + *) handle_error "Unsupported Ubuntu version: $UBUNTU_VERSION" ;; esac echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/packages.networkradius.com.asc] $REPO_URL" | sudo tee /etc/apt/sources.list.d/networkradius.list > /dev/null || handle_error "Failed to add NetworkRADIUS repository" log_success "NetworkRADIUS repository configured for Ubuntu $UBUNTU_VERSION" COMPLETED_STEPS+=("NetworkRADIUS repository configured") -# Set Valkey Repository -log_step "Adding Valkey repository" -if [[ "$UBUNTU_VERSION" == "focal" || "$UBUNTU_VERSION" == "jammy" ]]; then - # Remove conflicting redis packages - apt-get remove -y redis-tools redis-server || true - - # Fetch Percona release package - wget https://repo.percona.com/apt/percona-release_latest.$(lsb_release -sc)_all.deb - - # Install Percona release package - dpkg -i percona-release_latest.$(lsb_release -sc)_all.deb - - # Enable Percona repository for Valkey - percona-release enable valkey experimental - - # Update package list - apt-get update -fi - -COMPLETED_STEPS+=("Valkey repository added") - - +# Set environment variable to avoid interactive prompts +export DEBIAN_FRONTEND=noninteractive # Update and upgrade system log_step "Updating system packages" @@ -169,105 +130,52 @@ apt-get update || handle_error "Failed to update package lists" apt-get upgrade -y || handle_error "Failed to upgrade packages" COMPLETED_STEPS+=("System packages updated") -# Install required packages +# Install required packages (Cleaned virtual PHP packages and freeradius-rest) log_step "Installing required packages" +INSTALL_PACKAGES="nginx-full python3-certbot-nginx php${PHP_VERSION}-fpm php${PHP_VERSION}-mysql php${PHP_VERSION}-cli php${PHP_VERSION}-curl php${PHP_VERSION}-zip php${PHP_VERSION}-common php${PHP_VERSION}-gd php${PHP_VERSION}-mbstring php${PHP_VERSION}-xml php${PHP_VERSION}-dev php${PHP_VERSION}-bcmath php${PHP_VERSION}-intl php${PHP_VERSION}-redis git unzip curl wget software-properties-common apt-transport-https ca-certificates gnupg lsb-release supervisor valkey-server valkey-tools valkey-redis-compat valkey-sentinel ufw openvpn easy-rsa freeradius freeradius-mysql freeradius-utils mariadb-server mariadb-client" + if [ "$REINSTALL" = true ]; then log_info "Reinstalling packages (forcing configuration file replacement)" - apt-get install --reinstall -y -o Dpkg::Options::="--force-confmiss" \ - nginx-full \ - python3-certbot-nginx \ - php${PHP_VERSION}-fpm \ - php${PHP_VERSION}-mysql \ - php${PHP_VERSION}-cli \ - php${PHP_VERSION}-curl \ - php${PHP_VERSION}-zip \ - php${PHP_VERSION}-common \ - php${PHP_VERSION}-gd \ - php${PHP_VERSION}-mbstring \ - php${PHP_VERSION}-xml \ - php${PHP_VERSION}-dev \ - php${PHP_VERSION}-bcmath \ - php${PHP_VERSION}-intl \ - php${PHP_VERSION}-redis \ - php${PHP_VERSION}-tokenizer \ - php${PHP_VERSION}-ctype \ - php${PHP_VERSION}-fileinfo \ - php${PHP_VERSION}-json \ - git \ - unzip \ - curl \ - wget \ - software-properties-common \ - apt-transport-https \ - ca-certificates \ - gnupg \ - lsb-release \ - supervisor \ - valkey \ - valkey-compat \ - openvpn \ - easy-rsa \ - freeradius \ - freeradius-mysql \ - freeradius-utils \ - mariadb-server \ - mariadb-client || handle_error "Failed to reinstall packages" + apt-get install --reinstall -y -o Dpkg::Options::="--force-confmiss" $INSTALL_PACKAGES || handle_error "Failed to reinstall packages" else - apt-get install -y \ - nginx-full \ - python3-certbot-nginx \ - php${PHP_VERSION}-fpm \ - php${PHP_VERSION}-mysql \ - php${PHP_VERSION}-cli \ - php${PHP_VERSION}-curl \ - php${PHP_VERSION}-zip \ - php${PHP_VERSION}-common \ - php${PHP_VERSION}-gd \ - php${PHP_VERSION}-mbstring \ - php${PHP_VERSION}-xml \ - php${PHP_VERSION}-dev \ - php${PHP_VERSION}-bcmath \ - php${PHP_VERSION}-intl \ - php${PHP_VERSION}-redis \ - php${PHP_VERSION}-tokenizer \ - php${PHP_VERSION}-ctype \ - php${PHP_VERSION}-fileinfo \ - php${PHP_VERSION}-json \ - git \ - unzip \ - curl \ - wget \ - software-properties-common \ - apt-transport-https \ - ca-certificates \ - gnupg \ - lsb-release \ - supervisor \ - valkey \ - valkey-compat \ - openvpn \ - easy-rsa \ - freeradius \ - freeradius-mysql \ - freeradius-utils \ - mariadb-server \ - mariadb-client || handle_error "Failed to install packages" + apt-get install -y $INSTALL_PACKAGES || handle_error "Failed to install packages" fi COMPLETED_STEPS+=("Required packages installed") -# Configure Valkey with optimal settings for FreeRADIUS +# Configure Valkey service overrides +log_step "Configuring Valkey service overrides" +VKEY_OVERRIDE_DIR="/etc/systemd/system/valkey.service.d" +VKEY_OVERRIDE_FILE="${VKEY_OVERRIDE_DIR}/override.conf" + +mkdir -p "$VKEY_OVERRIDE_DIR" +cat > "$VKEY_OVERRIDE_FILE" << 'EOF' +[Unit] + +[Service] +# Increase timeouts to prevent premature termination +TimeoutStartSec=300 +TimeoutStopSec=300 + +# Ensure service restarts on failure +Restart=always +RestartSec=10s + +# Disable OOM killer for Valkey +OOMScoreAdjust=-1000 +EOF + +# Apply changes and restart Valkey +log_info "Applying Valkey service configuration..." + +# Configure Valkey with optimized settings for FreeRADIUS log_step "Configuring Valkey with optimized settings" -# Calculate optimal memory allocation (75% of available RAM, capped at 3GB, minimum 1GB) +# Calculate optimal memory allocation (Safe 15% of available RAM, capped at 3GB, minimum 1GB) TOTAL_RAM_KB=$(grep MemTotal /proc/meminfo | awk '{print $2}') TOTAL_RAM_MB=$((TOTAL_RAM_KB / 1024)) -MAX_MEMORY_MB=$((TOTAL_RAM_MB * 75 / 100)) -if [ "$MAX_MEMORY_MB" -gt 3072 ]; then - MAX_MEMORY_MB=3072 # Cap at 3GB -fi -if [ "$MAX_MEMORY_MB" -lt 1024 ]; then - MAX_MEMORY_MB=1024 # Minimum 1GB -fi +MAX_MEMORY_MB=$((TOTAL_RAM_MB * 15 / 100)) +if [ "$MAX_MEMORY_MB" -gt 3072 ]; then MAX_MEMORY_MB=3072; fi +if [ "$MAX_MEMORY_MB" -lt 1024 ]; then MAX_MEMORY_MB=1024; fi log_info "Configuring Valkey with ${MAX_MEMORY_MB}MB memory allocation" @@ -277,7 +185,6 @@ mkdir -p /etc/valkey || handle_error "Failed to create Valkey configuration dire # Configure Valkey with optimized settings for FreeRADIUS cat > /etc/valkey/valkey.conf << EOL # Valkey configuration for FreeRADIUS - bind 0.0.0.0 ::0 protected-mode yes port 6379 @@ -316,8 +223,7 @@ dbfilename dump.rdb # Disable RDB snapshots since we're using AOF save "" -# Security -# Reuse the same password as MySQL for simplicity +# Security (Reuse the same password as MySQL for simplicity) requirepass "$MYSQL_PASSWORD" # Network @@ -326,8 +232,6 @@ repl-timeout 60 repl-ping-slave-period 10 repl-backlog-size 1mb repl-backlog-ttl 3600 - -# Client timeouts timeout 0 tcp-keepalive 300 @@ -337,39 +241,23 @@ rename-command FLUSHALL "" rename-command CONFIG "" rename-command SHUTDOWN "" -# Set the number of threads to serve the requests +# Tune threads and clients io-threads 2 io-threads-do-reads yes - -# Set the max number of connected clients at the same time maxclients 10000 -# Set the threshold for keys with an expire set to be considered for deletion +# Tune data structures active-expire-effort 1 - -# Set the threshold for client output buffer limits client-output-buffer-limit normal 0 0 0 client-output-buffer-limit replica 256mb 64mb 60 client-output-buffer-limit pubsub 32mb 8mb 60 - -# Tune hash data structure hash-max-ziplist-entries 512 hash-max-ziplist-value 64 - -# Tune list data structure list-max-ziplist-size -2 - -# Tune set data structure set-max-intset-entries 512 - -# Tune zset data structure zset-max-ziplist-entries 128 zset-max-ziplist-value 64 - -# Tune hll data structure hll-sparse-max-bytes 3000 - -# Tune stream data structure stream-node-max-bytes 4096 stream-node-max-entries 100 @@ -382,41 +270,27 @@ active-defrag-cycle-max 75 active-defrag-max-scan-fields 1000 EOL -# Set proper permissions for Valkey directories -log_step "Setting Valkey directory permissions" -mkdir -p /var/lib/valkey/appendonlydir -chown -R valkey:valkey /var/lib/valkey /var/log/valkey /var/run/valkey -chmod 750 /var/lib/valkey /var/log/valkey /var/run/valkey - -# Fix Valkey service -sed -i 's/ConditionPathExists=!\/etc\/valkey\/REDIS_MIGRATION/ConditionPathExists=\/etc\/valkey\/REDIS_MIGRATION/g' /usr/lib/systemd/system/valkey.service - # Restart Valkey to apply new configuration systemctl daemon-reexec || log_warning "daemon-reexec failed (non-critical)" systemctl daemon-reload || handle_error "Failed to reload systemd daemon" -systemctl restart valkey || handle_error "Failed to restart Valkey" -systemctl enable valkey || handle_error "Failed to enable Valkey" +systemctl restart valkey-server || handle_error "Failed to restart Valkey" +systemctl enable valkey-server || handle_error "Failed to enable Valkey" # Verify Valkey is running log_step "Verifying Valkey service status" - -# Check service status -if systemctl is-active --quiet valkey; then +if systemctl is-active --quiet valkey-server; then log_success "Valkey service is running" COMPLETED_STEPS+=("Valkey configured with ${MAX_MEMORY_MB}MB memory allocation") else - # If service is not running, try to get more information log_warning "Valkey service is not running as expected. Checking status..." - systemctl status valkey --no-pager || true + systemctl status valkey-server --no-pager || true - # Try to start the service log_info "Attempting to start Valkey service..." - if systemctl start valkey; then + if systemctl start valkey-server; then log_success "Successfully started Valkey service" COMPLETED_STEPS+=("Valkey configured with ${MAX_MEMORY_MB}MB memory allocation") else - # If we still can't start, show detailed error but don't fail the script - log_error "Failed to start Valkey service. Please check the logs with: journalctl -u valkey -n 50" + log_error "Failed to start Valkey service. Please check the logs with: journalctl -u valkey-server -n 50" log_warning "Continuing installation despite Valkey service issue..." COMPLETED_STEPS+=("Valkey configuration completed but service failed to start") fi @@ -435,52 +309,26 @@ systemctl status valkey --no-pager -l echo -e "\n=== Valkey Key Statistics ===" echo "Total Keys in DB 0: $(valkey-cli -h $VALKEY_HOST -p $VALKEY_PORT dbsize)" - EOF -chmod +x /usr/local/bin/valkey-debug.sh - +chmod +x /usr/local/bin/valkey-debug.sh || handle_error "Failed to make Valkey debug script executable" COMPLETED_STEPS+=("Valkey monitoring configured") -# Add monitoring cron job +# Add monitoring cron job (Safe Append Fix) log_step "Adding monitoring cron job" -echo "*/5 * * * * /usr/local/bin/valkey-debug.sh" > cronjob || handle_error "Failed to write monitoring cron job to temporary file" -crontab cronjob || handle_error "Failed to install monitoring cron job" -rm cronjob || handle_error "Failed to remove temporary monitoring cron job file" +(crontab -l 2>/dev/null; echo "*/5 * * * * /usr/local/bin/valkey-debug.sh") | crontab - || handle_error "Failed to install monitoring cron job" COMPLETED_STEPS+=("Monitoring cron job added") # Verify Valkey is working log_step "Verifying Valkey installation" -if ! systemctl is-active --quiet valkey; then -handle_error "Valkey service is not running" -fi +if ! systemctl is-active --quiet valkey; then handle_error "Valkey service is not running"; fi # Test Valkey connectivity and basic operations -if [ "$(valkey-cli ping)" != "PONG" ]; then -handle_error "Valkey is not responding to ping" -fi - -# Test Valkey write operation -if [ "$(valkey-cli set test_key test_value)" != "OK" ]; then -handle_error "Valkey write operation failed" -fi - -# Test Valkey read operation -TEST_VALUE=$(valkey-cli get test_key) -if [ "$TEST_VALUE" != "test_value" ]; then -handle_error "Valkey read operation failed" -fi - -# Test Valkey delete operation -if [ "$(valkey-cli del test_key)" != "1" ]; then -handle_error "Valkey delete operation failed" -fi - -# Check Valkey info for basic stats -if ! valkey-cli info | grep -q "valkey_version"; then -handle_error "Unable to get Valkey server information" -fi - +if [ "$(valkey-cli ping)" != "PONG" ]; then handle_error "Valkey is not responding to ping"; fi +if [ "$(valkey-cli set test_key test_value)" != "OK" ]; then handle_error "Valkey write operation failed"; fi +if [ "$(valkey-cli get test_key)" != "test_value" ]; then handle_error "Valkey read operation failed"; fi +if [ "$(valkey-cli del test_key)" != "1" ]; then handle_error "Valkey delete operation failed"; fi +if ! valkey-cli info | grep -q "valkey_version"; then handle_error "Unable to get Valkey server information"; fi COMPLETED_STEPS+=("Valkey functionality verified") # Set Default PHP Version @@ -490,27 +338,16 @@ COMPLETED_STEPS+=("PHP ${PHP_VERSION} set as default") # Install and configure ionCube Loader log_step "Installing ionCube Loader" - -# Check if ionCube is already installed if [ -d "/usr/local/ioncube" ] && [ -f "/usr/local/ioncube/ioncube_loader_lin_${PHP_VERSION}.so" ]; then log_info "ionCube Loader already exists, skipping download and installation" COMPLETED_STEPS+=("ionCube Loader reused (already exists)") else log_info "ionCube Loader not found, downloading and installing" - - # Change to /tmp directory for downloads cd /tmp || handle_error "Failed to change to /tmp directory" - - # Download and extract ionCube wget -O ioncube.zip "https://downloads.ioncube.com/loader_downloads/ioncube_loaders_lin_x86-64.zip" || handle_error "Failed to download ionCube" unzip -q ioncube.zip || handle_error "Failed to extract ionCube" - - # Remove existing ionCube directory if it exists rm -rf /usr/local/ioncube 2>/dev/null - - # Move the ioncube directory to /usr/local mv ioncube /usr/local/ || handle_error "Failed to move ionCube to /usr/local" - COMPLETED_STEPS+=("ionCube Loader downloaded and installed") fi @@ -522,8 +359,6 @@ EOL # Enable ionCube for PHP CLI and FPM ln -sf /etc/php/${PHP_VERSION}/mods-available/ioncube.ini /etc/php/${PHP_VERSION}/cli/conf.d/00-ioncube.ini || handle_error "Failed to enable ionCube for CLI" ln -sf /etc/php/${PHP_VERSION}/mods-available/ioncube.ini /etc/php/${PHP_VERSION}/fpm/conf.d/00-ioncube.ini || handle_error "Failed to enable ionCube for FPM" - -# Restart PHP-FPM to load ionCube systemctl restart php${PHP_VERSION}-fpm || handle_error "Failed to restart PHP-FPM" # Verify ionCube installation @@ -536,8 +371,6 @@ fi # Start and enable MariaDB log_step "Configuring MariaDB" - -# Initialize MariaDB system database if not already done if [ ! -d "/var/lib/mysql/mysql" ]; then log_info "Initializing MariaDB system database" mysql_install_db --user=mysql --datadir=/var/lib/mysql || handle_error "Failed to initialize MariaDB" @@ -551,101 +384,60 @@ if [ ! -f "/etc/mysql/debian-start" ]; then log_info "Creating missing /etc/mysql/debian-start script" cat > /etc/mysql/debian-start << 'EOF' #!/bin/bash -# This script is executed by "/etc/init.d/mysql" on every (re)start. - -# Exit if the script is not being run by root -if [ "$(id -u)" != "0" ]; then - echo "This script must be run as root" 1>&2 - exit 1 -fi - -# Exit successfully if mysql is not running -if ! /usr/bin/mysqladmin --defaults-file=/etc/mysql/debian.cnf ping > /dev/null 2>&1; then - exit 0 -fi - -# Exit successfully +if [ "$(id -u)" != "0" ]; then echo "This script must be run as root" 1>&2; exit 1; fi +if ! /usr/bin/mysqladmin --defaults-file=/etc/mysql/debian.cnf ping > /dev/null 2>&1; then exit 0; fi exit 0 EOF chmod +x /etc/mysql/debian-start || handle_error "Failed to make debian-start executable" log_success "Created /etc/mysql/debian-start script" fi - COMPLETED_STEPS+=("MariaDB initialized, started and enabled") -# Configure MySQL to allow remote connections and optimize performance -log_step "Configuring MySQL for remote connections and performance" +# Configure MySQL to allow remote connections +log_step "Configuring MySQL for remote connections" +mkdir -p /etc/mysql/mariadb.conf.d/ || handle_error "Failed to create MariaDB conf directory" -# Create MariaDB configuration directory if it doesn't exist -mkdir -p /etc/mysql/mariadb.conf.d/ - -# Configure MariaDB cat > /etc/mysql/mariadb.conf.d/50-server.cnf << 'EOL' -[mysqld] +[server] +[mariadbd] user = mysql pid-file = /run/mysqld/mysqld.pid -socket = /run/mysqld/mysqld.sock -port = 3306 basedir = /usr datadir = /var/lib/mysql tmpdir = /tmp -lc-messages-dir = /usr/share/mysql -lc-messages = en_US skip-external-locking - bind-address = 0.0.0.0 - key_buffer_size = 16M max_allowed_packet = 16M thread_stack = 192K thread_cache_size = 8 - myisam-recover-options = BACKUP - query_cache_limit = 1M query_cache_size = 16M - expire_logs_days = 10 max_binlog_size = 100M - character-set-server = utf8mb4 collation-server = utf8mb4_general_ci - -# Performance optimizations innodb_buffer_pool_size = 1G innodb_log_file_size = 256M innodb_flush_method = O_DIRECT innodb_flush_log_at_trx_commit = 2 innodb_file_per_table = 1 - [embedded] - [mariadb] - -[mariadb-10.6] EOL -# Restart MariaDB to apply changes systemctl restart mariadb || handle_error "Failed to restart MariaDB after configuration change" COMPLETED_STEPS+=("MySQL configured for remote connections") -# Get server hostname and set email -DOMAIN=$(hostname -f) -EMAIL_ADDRESS="simpluxsolutions@gmail.com" - # Generate random credentials or reuse existing ones DB_CREDENTIALS_FILE="/root/db.txt" - -# Check if db.txt exists and contains valid credentials if [ -f "$DB_CREDENTIALS_FILE" ] && [ -r "$DB_CREDENTIALS_FILE" ]; then log_step "Found existing database credentials, reusing them" - - # Extract credentials from existing db.txt file MYSQL_USER=$(grep "^DB_USERNAME=" "$DB_CREDENTIALS_FILE" | cut -d'=' -f2) MYSQL_PASSWORD=$(grep "^DB_PASSWORD=" "$DB_CREDENTIALS_FILE" | cut -d'=' -f2) MYSQL_DATABASE=$(grep "^DB_DATABASE=" "$DB_CREDENTIALS_FILE" | cut -d'=' -f2) - # Validate that we got all required credentials if [ -n "$MYSQL_USER" ] && [ -n "$MYSQL_PASSWORD" ] && [ -n "$MYSQL_DATABASE" ]; then log_info "Reusing existing database credentials: User=$MYSQL_USER, Database=$MYSQL_DATABASE" COMPLETED_STEPS+=("Database credentials reused from existing file") @@ -664,6 +456,16 @@ else COMPLETED_STEPS+=("New database credentials generated") fi +# Save database credentials +log_step "Saving database credentials" +echo "MySQL Credentials:" > "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" +echo "DB_HOST=localhost" >> "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" +echo "DB_PORT=3306" >> "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" +echo "DB_DATABASE=$MYSQL_DATABASE" >> "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" +echo "DB_USERNAME=$MYSQL_USER" >> "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" +echo "DB_PASSWORD=$MYSQL_PASSWORD" >> "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" +COMPLETED_STEPS+=("Database credentials saved") + # Secure MariaDB installation log_step "Securing MariaDB installation" mysql -e "DELETE FROM mysql.user WHERE User='';" || handle_error "Failed to delete anonymous MariaDB user" @@ -676,13 +478,8 @@ COMPLETED_STEPS+=("MariaDB installation secured") # Create database and user log_step "Creating database and user" mysql -e "CREATE DATABASE $MYSQL_DATABASE;" || handle_error "Failed to create database" - -# Create user with access from any host mysql -e "CREATE USER '$MYSQL_USER'@'%' IDENTIFIED BY '$MYSQL_PASSWORD';" || handle_error "Failed to create database user" - -# Grant privileges for all hosts mysql -e "GRANT ALL PRIVILEGES ON $MYSQL_DATABASE.* TO '$MYSQL_USER'@'%';" || handle_error "Failed to grant database privileges" - mysql -e "FLUSH PRIVILEGES;" || handle_error "Failed to flush MariaDB privileges" COMPLETED_STEPS+=("Database and user created with full access") @@ -697,20 +494,68 @@ if ! command -v composer &> /dev/null; then fi COMPLETED_STEPS+=("Composer installed") +# Configure Nginx +log_step "Configuring Nginx" +mv /etc/nginx/sites-available/default /etc/nginx/sites-available/default.bak || handle_error "Failed to backup default Nginx site" +touch /etc/nginx/sites-available/default || handle_error "Failed to create new Nginx site" +ln -sf /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default || handle_error "Failed to symlink Nginx site" + +cat > /etc/nginx/sites-available/default << EOL +server { + listen 80; + listen [::]:80; + + root /var/www/html/public; + index index.php index.html index.htm index.nginx-debian.html; + + server_name $DOMAIN; + + location / { + try_files \$uri \$uri/ /index.php?\$query_string; + } + + location ~ \.php$ { + include snippets/fastcgi-php.conf; + fastcgi_pass unix:/var/run/php/php${PHP_VERSION}-fpm.sock; + } + + location ~ /\.ht { + deny all; + } +} +EOL +COMPLETED_STEPS+=("Nginx configured") + +# Configure SSL with Certbot +log_step "Configuring SSL with Certbot" +echo "Configuring SSL certificate for $DOMAIN" + +if [ -d "/etc/letsencrypt/live/$DOMAIN" ]; then + log_info "SSL certificate already exists for $DOMAIN, reusing existing certificate" + echo -e "1\n" | certbot --nginx -d "$DOMAIN" || handle_error "Failed to reuse SSL certificate" + COMPLETED_STEPS+=("SSL certificate reused (already exists)") +else + log_info "No existing SSL certificate found for $DOMAIN, requesting new certificate" + certbot --nginx -d "$DOMAIN" --agree-tos --email "$EMAIL_ADDRESS" --no-eff-email --non-interactive --redirect || handle_error "Failed to configure SSL with Certbot" + COMPLETED_STEPS+=("SSL configured with Certbot") +fi + +# Test the Nginx configuration (Graceful restart applied) +log_step "Restarting Nginx" +nginx -t || handle_error "Nginx configuration failed" +systemctl restart nginx || handle_error "Failed to restart Nginx" +COMPLETED_STEPS+=("Nginx restarted gracefully") # Setup Laravel application log_step "Setting up Laravel application" LOCAL_PATH="/var/www/html" REPO_URL="$GITHUB_REPO_URL" - -# Remove existing web root if it exists (no backup) if [ -d "$LOCAL_PATH" ]; then rm -rf "$LOCAL_PATH" || handle_error "Failed to remove existing web root" fi -# Clone the repository -git clone "$REPO_URL" "$LOCAL_PATH" || handle_error "Failed to clone repository" +git clone -b master "$REPO_URL" "$LOCAL_PATH" || handle_error "Failed to clone repository" cd "$LOCAL_PATH" || handle_error "Failed to change directory to web root" # Install Laravel dependencies @@ -734,83 +579,45 @@ sed -i "s|APP_URL=.*|APP_URL=https://$DOMAIN|" .env || handle_error "Failed to u sed -i "s|APP_NAME=.*|APP_NAME=\"$DOMAIN\"|" .env || handle_error "Failed to update APP_NAME in .env" COMPLETED_STEPS+=(".env file updated with database credentials") - -# Restart services -log_step "Restarting services" -systemctl restart mariadb || handle_error "Failed to restart MariaDB" -systemctl restart php${PHP_VERSION}-fpm || handle_error "Failed to restart PHP ${PHP_VERSION} FPM" -systemctl restart supervisor || handle_error "Failed to restart Supervisor" -systemctl restart openvpn || handle_error "Failed to restart OpenVPN" -COMPLETED_STEPS+=("Services restarted") - # Run Laravel migrations and seed the database log_step "Running Laravel migrations and seeding database" php artisan migrate --force || handle_error "Failed to run Laravel migrations" php artisan db:seed --force || handle_error "Failed to seed database" COMPLETED_STEPS+=("Laravel migrations run and database seeded") +# Set correct www permissions +log_step "Setting correct www permissions" +chown -R www-data:www-data /var/www/html || handle_error "Failed to set ownership of web root" +chmod -R 775 /var/www/html/storage || handle_error "Failed to set permissions of storage directory" +chmod -R 775 /var/www/html/bootstrap/cache || handle_error "Failed to set permissions of cache directory" + +# Make the scripts executable +chmod +x /var/www/html/sh/set_permissions.sh || handle_error "Failed to make permissions script executable" +chmod +x /var/www/html/sh/restart-services.sh || handle_error "Failed to make services script executable" + +# Run the script once to apply initial configuration +/var/www/html/sh/set_permissions.sh || handle_error "Failed to run permissions script" +/var/www/html/sh/restart-services.sh || handle_error "Failed to run services script" +COMPLETED_STEPS+=("Correct www permissions set") + # Optimize RADIUS database indexes log_step "Optimizing RADIUS database indexes" cat > /tmp/radius_optimize.sql << "EOL" USE radius; - --- Add indexes to improve query performance - --- radcheck -ALTER TABLE radcheck - ADD INDEX idx_username (username), - ADD INDEX idx_attribute (attribute); +ALTER TABLE radcheck ADD INDEX idx_username (username), ADD INDEX idx_attribute (attribute); ANALYZE TABLE radcheck; - --- radreply -ALTER TABLE radreply - ADD INDEX idx_username (username), - ADD INDEX idx_attribute (attribute); +ALTER TABLE radreply ADD INDEX idx_username (username), ADD INDEX idx_attribute (attribute); ANALYZE TABLE radreply; - --- radusergroup -ALTER TABLE radusergroup - ADD INDEX idx_username (username), - ADD INDEX idx_groupname (groupname); +ALTER TABLE radusergroup ADD INDEX idx_username (username), ADD INDEX idx_groupname (groupname); ANALYZE TABLE radusergroup; - --- radgroupcheck -ALTER TABLE radgroupcheck - ADD INDEX idx_groupname (groupname), - ADD INDEX idx_attribute (attribute); +ALTER TABLE radgroupcheck ADD INDEX idx_groupname (groupname), ADD INDEX idx_attribute (attribute); ANALYZE TABLE radgroupcheck; - --- radgroupreply -ALTER TABLE radgroupreply - ADD INDEX idx_groupname (groupname), - ADD INDEX idx_attribute (attribute); +ALTER TABLE radgroupreply ADD INDEX idx_groupname (groupname), ADD INDEX idx_attribute (attribute); ANALYZE TABLE radgroupreply; - --- radacct (very critical for performance) -ALTER TABLE radacct - ADD INDEX idx_username (username), - ADD INDEX idx_acctsessionid (acctsessionid), - ADD INDEX idx_framedipaddress (framedipaddress), - ADD INDEX idx_acctstarttime (acctstarttime), - ADD INDEX idx_acctstoptime (acctstoptime), - ADD INDEX idx_nasipaddress (nasipaddress), - ADD INDEX idx_calledstationid (calledstationid), - ADD INDEX idx_callingstationid (callingstationid); +ALTER TABLE radacct ADD INDEX idx_username (username), ADD INDEX idx_acctsessionid (acctsessionid), ADD INDEX idx_framedipaddress (framedipaddress), ADD INDEX idx_acctstarttime (acctstarttime), ADD INDEX idx_acctstoptime (acctstoptime), ADD INDEX idx_nasipaddress (nasipaddress), ADD INDEX idx_calledstationid (calledstationid), ADD INDEX idx_callingstationid (callingstationid); ANALYZE TABLE radacct; - --- radpostauth -ALTER TABLE radpostauth - ADD INDEX idx_username (username), - ADD INDEX idx_reply (reply), - ADD INDEX idx_authdate (authdate); +ALTER TABLE radpostauth ADD INDEX idx_username (username), ADD INDEX idx_reply (reply), ADD INDEX idx_authdate (authdate); ANALYZE TABLE radpostauth; - --- hotspot_sessions -ALTER TABLE hotspot_sessions - ADD INDEX idx_payment_voucher (payment_id, voucher); -ANALYZE TABLE hotspot_sessions; - --- Convert tables to InnoDB and utf8mb4 (recommended for reliability and Unicode support) ALTER TABLE radcheck ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE radreply ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE radusergroup ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; @@ -818,9 +625,6 @@ ALTER TABLE radgroupcheck ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLAT ALTER TABLE radgroupreply ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE radacct ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE radpostauth ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -ALTER TABLE hotspot_sessions ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - --- Analyze again after engine/charset conversion ANALYZE TABLE radcheck; ANALYZE TABLE radreply; ANALYZE TABLE radusergroup; @@ -828,66 +632,128 @@ ANALYZE TABLE radgroupcheck; ANALYZE TABLE radgroupreply; ANALYZE TABLE radacct; ANALYZE TABLE radpostauth; -ANALYZE TABLE hotspot_sessions; - EOL mysql -u root < /tmp/radius_optimize.sql || handle_error "Failed to optimize RADIUS database indexes" -rm -f /tmp/radius_optimize.sql +rm -f /tmp/radius_optimize.sql || handle_error "Failed to remove radius optimization sql file" COMPLETED_STEPS+=("RADIUS database indexes optimized") -# Configure FreeRADIUS -log_step "Configuring FreeRADIUS" +# Configure Supervisor for queue worker (Pre-created log dir fix) +log_step "Configuring Supervisor for queue worker" +mkdir -p /var/www/html/storage/logs || handle_error "Failed to create Laravel logs directory" +cat > /etc/supervisor/conf.d/queue-worker.conf << "EOL" +[program:queue-worker] +process_name=%(program_name)s_%(process_num)02d +command=php /var/www/html/artisan queue:work --tries=3 +autostart=true +autorestart=true +stopasgroup=true +killasgroup=true +user=www-data +numprocs=5 +redirect_stderr=true +stdout_logfile=/var/www/html/storage/logs/queue-worker.log +EOL +COMPLETED_STEPS+=("Supervisor configured for queue worker") -# Enable buffered-sql site -log_step "Enabling buffered-sql site" -# Ensure the sites-enabled directory exists -mkdir -p /etc/freeradius/sites-enabled || handle_error "Failed to create FreeRADIUS sites-enabled directory" -ln -sf /etc/freeradius/sites-available/buffered-sql /etc/freeradius/sites-enabled/buffered-sql || handle_error "Failed to enable buffered-sql site" -COMPLETED_STEPS+=("FreeRADIUS buffered-sql site enabled") +# Install OpenVPN based on Ubuntu version +log_step "Installing OpenVPN" +case $UBUNTU_VERSION in + "focal"|"jammy"|"noble") + echo "Installing OpenVPN for Ubuntu $UBUNTU_VERSION" + export AUTO_INSTALL=y + curl -O https://raw.githubusercontent.com/simpleisp/bash/main/openvpn.sh || handle_error "Failed to download OpenVPN installer" + chmod +x openvpn.sh || handle_error "Failed to make OpenVPN installer executable" + ./openvpn.sh || handle_error "Failed to install OpenVPN" -# Enable SQL module for FreeRADIUS -log_step "Enabling SQL module" -# Ensure the mods-enabled directory exists -mkdir -p /etc/freeradius/mods-enabled || handle_error "Failed to create FreeRADIUS mods-enabled directory" -ln -sf /etc/freeradius/mods-available/sql /etc/freeradius/mods-enabled/sql || handle_error "Failed to enable SQL module" -COMPLETED_STEPS+=("FreeRADIUS SQL module enabled") + # Set more secure permissions for OpenVPN + chown -R root:root /etc/openvpn || handle_error "Failed to set ownership of OpenVPN configuration directory" + chmod -R 777 /etc/openvpn || handle_error "Failed to set permissions of OpenVPN configuration directory" + chmod -R 777 /etc/openvpn/easy-rsa || handle_error "Failed to set permissions of OpenVPN easy-rsa directory" + ;; + *) + handle_error "Unsupported Ubuntu version for OpenVPN installation" + ;; +esac +COMPLETED_STEPS+=("OpenVPN installed") -# Configure FreeRADIUS REST module -log_step "Configuring FreeRADIUS REST module" -REST_CONFIG="/etc/freeradius/mods-available/rest" +# Configure Systemd sandbox overrides for OpenVPN writes +log_step "Configuring Systemd sandbox overrides for OpenVPN writes" +mkdir -p /etc/systemd/system/php${PHP_VERSION}-fpm.service.d || handle_error "Failed to create PHP systemd override directory" +cat > /etc/systemd/system/php${PHP_VERSION}-fpm.service.d/override.conf << 'EOF' +[Service] +ReadWritePaths=/etc/openvpn +EOF -# REST module disabled for SimpleISP to avoid connection errors during configuration test -#rm /etc/freeradius/mods-enabled/rest || handle_error "Failed to disable REST module" +mkdir -p /etc/systemd/system/supervisor.service.d || handle_error "Failed to create Supervisor systemd override directory" +cat > /etc/systemd/system/supervisor.service.d/override.conf << 'EOF' +[Service] +ReadWritePaths=/etc/openvpn +EOF +COMPLETED_STEPS+=("Systemd sandbox overrides configured") -if [ -f "$REST_CONFIG" ]; then - # Update connect_uri to use domain/api instead of localhost - sed -i 's|connect_uri = "http://127.0.0.1/"|connect_uri = "https://'$DOMAIN'/api"|g' "$REST_CONFIG" || handle_error "Failed to configure REST module connect_uri" - # Also handle the commented version - sed -i 's|# connect_uri = "http://127.0.0.1/"|connect_uri = "https://'$DOMAIN'/api"|g' "$REST_CONFIG" || true -fi +# Install Laravel cron (Safe Append Fix) +log_step "Installing cron" +(crontab -l 2>/dev/null; echo "* * * * * php /var/www/html/artisan schedule:run >> /dev/null 2>&1") | crontab - || handle_error "Failed to install Laravel cron job" +COMPLETED_STEPS+=("Cron job installed") -COMPLETED_STEPS+=("FreeRADIUS REST module configured (disabled for SimpleISP)") +# Update sudoers for www-data user (Updated openvpn explicit target) +log_step "Updating sudoers for www-data user" +cat >> /etc/sudoers << 'EOL' +www-data ALL=NOPASSWD: /bin/systemctl start openvpn@server +www-data ALL=NOPASSWD: /bin/systemctl stop openvpn@server +www-data ALL=NOPASSWD: /bin/systemctl restart openvpn@server +www-data ALL=NOPASSWD: /bin/systemctl status openvpn@server +www-data ALL=NOPASSWD: /bin/systemctl reload openvpn@server +www-data ALL=NOPASSWD: /bin/systemctl enable openvpn@server +www-data ALL=NOPASSWD: /bin/systemctl disable openvpn@server +www-data ALL=NOPASSWD: /bin/systemctl start freeradius +www-data ALL=NOPASSWD: /bin/systemctl stop freeradius +www-data ALL=NOPASSWD: /bin/systemctl restart freeradius +www-data ALL=NOPASSWD: /bin/systemctl status freeradius +www-data ALL=NOPASSWD: /bin/systemctl reload freeradius +www-data ALL=NOPASSWD: /bin/systemctl enable freeradius +www-data ALL=NOPASSWD: /bin/systemctl disable freeradius +www-data ALL=NOPASSWD: /bin/supervisorctl stop all +www-data ALL=NOPASSWD: /bin/supervisorctl reread +www-data ALL=NOPASSWD: /bin/supervisorctl update +www-data ALL=NOPASSWD: /bin/supervisorctl start all +www-data ALL=NOPASSWD: /bin/supervisorctl restart all +www-data ALL=NOPASSWD: /bin/supervisorctl status +www-data ALL=NOPASSWD: /bin/systemctl restart supervisor +www-data ALL=NOPASSWD: /bin/systemctl status ssh +www-data ALL=NOPASSWD: /var/www/html/sh/set_permissions.sh +www-data ALL=NOPASSWD: /var/www/html/sh/restart-services.sh +EOL +COMPLETED_STEPS+=("Sudoers updated for www-data user") + +# Open Firewall Ports and enable ufw +log_step "Opening firewall ports and enabling ufw" +ufw allow ssh || handle_error "Failed to allow SSH through firewall" +ufw allow 9080/tcp || handle_error "Failed to allow port 9080 through firewall" +ufw allow http || handle_error "Failed to allow HTTP through firewall" +ufw allow https || handle_error "Failed to allow HTTPS through firewall" +ufw allow 1194/tcp || handle_error "Failed to allow OpenVPN through firewall" +ufw allow 1812:1813/udp || handle_error "Failed to allow FreeRADIUS through firewall" +ufw reload || handle_error "Failed to reload firewall rules" +yes | ufw enable || handle_error "Failed to enable firewall" +COMPLETED_STEPS+=("Firewall ports opened and ufw enabled") + +# Test FreeRADIUS configuration +log_step "Checking FreeRADIUS files" -# Ensure FreeRADIUS configuration files exist (restore if missing) -log_step "Checking FreeRADIUS radiusd.conf" if [ ! -f "/etc/freeradius/radiusd.conf" ]; then log_info "FreeRADIUS configuration files missing, reinstalling and reconfiguring FreeRADIUS package" - - # Purge and reinstall FreeRADIUS to ensure clean configuration apt-get purge -y freeradius freeradius-common freeradius-config 2>/dev/null || echo "FreeRADIUS not installed" apt-get autoremove -y 2>/dev/null - apt-get install -y freeradius freeradius-mysql freeradius-utils || handle_error "Failed to reinstall FreeRADIUS" - - # Reconfigure the package to ensure configuration files are created + apt-get install -y freeradius freeradius-mysql freeradius-config || handle_error "Failed to reinstall FreeRADIUS" dpkg-reconfigure -f noninteractive freeradius-config 2>/dev/null || echo "Reconfigure not needed" - - # Verify configuration file was created - if [ ! -f "/etc/freeradius/radiusd.conf" ]; then - # Create a minimal radiusd.conf if still missing - log_info "Creating minimal radiusd.conf configuration" - mkdir -p /etc/freeradius - cat > /etc/freeradius/radiusd.conf << 'EOF' +fi + +if [ ! -f "/etc/freeradius/radiusd.conf" ]; then + log_info "Creating minimal radiusd.conf configuration" + mkdir -p /etc/freeradius || handle_error "Failed to create freeradius config dir" + cat > /etc/freeradius/radiusd.conf << 'EOF' prefix = /usr exec_prefix = ${prefix} sysconfdir = /etc @@ -905,91 +771,83 @@ cadir = ${confdir}/certs run_dir = ${localstatedir}/run/${name} db_dir = ${raddbdir} - libdir = /usr/lib/freeradius - pidfile = ${run_dir}/${name}.pid correct_escapes = true - max_request_time = 30 cleanup_delay = 5 max_requests = 16384 - hostname_lookups = no log { - destination = files - colourise = yes - file = ${logdir}/radius.log - syslog_facility = daemon - stripped_names = no - auth = no - auth_badpass = no - auth_goodpass = no - msg_denied = "You are already logged in - access denied" +destination = files +colourise = yes +file = ${logdir}/radius.log +syslog_facility = daemon +stripped_names = no +auth = no +auth_badpass = no +auth_goodpass = no +msg_denied = "You are already logged in - access denied" } checkrad = ${sbindir}/checkrad security { - allow_core_dumps = no - max_attributes = 200 - reject_delay = 1 - status_server = yes +allow_core_dumps = no +max_attributes = 200 +reject_delay = 1 +status_server = yes } proxy_requests = yes $INCLUDE proxy.conf - $INCLUDE clients.conf thread pool { - start_servers = 5 - max_servers = 32 - min_spare_servers = 3 - max_spare_servers = 10 - max_requests_per_server = 0 - auto_limit_acct = no +start_servers = 5 +max_servers = 32 +min_spare_servers = 3 +max_spare_servers = 10 +max_requests_per_server = 0 +auto_limit_acct = no } $INCLUDE sites-enabled/ - $INCLUDE mods-enabled/ policy { - $INCLUDE policy.d/ +$INCLUDE policy.d/ } instantiate { } EOF - chmod 644 /etc/freeradius/radiusd.conf - log_success "Created minimal radiusd.conf configuration" - fi - - # Re-enable modules after reinstallation - if [ -f "/etc/freeradius/mods-available/sql" ]; then - ln -sf /etc/freeradius/mods-available/sql /etc/freeradius/mods-enabled/ || handle_error "Failed to re-enable SQL module" - fi - if [ -f "/etc/freeradius/mods-available/rest" ]; then - # REST module disabled for SimpleISP to avoid connection errors during configuration test - # ln -sf /etc/freeradius/mods-available/rest /etc/freeradius/mods-enabled/rest || handle_error "Failed to re-enable REST module" - log_info "REST module configuration skipped for SimpleISP" - fi - log_success "FreeRADIUS configuration files restored" + chmod 644 /etc/freeradius/radiusd.conf || handle_error "Failed to set radiusd.conf permissions" + log_success "Created minimal radiusd.conf configuration" +fi + +if [ -f "/etc/freeradius/mods-available/sql" ]; then + ln -sf /etc/freeradius/mods-available/sql /etc/freeradius/mods-enabled/ || handle_error "Failed to re-enable SQL module" fi +COMPLETED_STEPS+=("Completed checking FreeRADIUS files") + +# Enable buffered-sql site +log_step "Enabling FreeRADIUS buffered-sql site" +mkdir -p /etc/freeradius/sites-enabled || handle_error "Failed to create FreeRADIUS sites-enabled directory" +ln -sf /etc/freeradius/sites-available/buffered-sql /etc/freeradius/sites-enabled/buffered-sql || handle_error "Failed to enable buffered-sql site" +COMPLETED_STEPS+=("FreeRADIUS buffered-sql site enabled") -COMPLETED_STEPS+=("Completed checking radiusd.conf") +# Enable SQL module for FreeRADIUS +log_step "Enabling SQL module" +mkdir -p /etc/freeradius/mods-enabled || handle_error "Failed to create FreeRADIUS mods-enabled directory" +ln -sf /etc/freeradius/mods-available/sql /etc/freeradius/mods-enabled/sql || handle_error "Failed to enable SQL module" +COMPLETED_STEPS+=("FreeRADIUS SQL module enabled") # Write new FreeRADIUS SQL module log_step "Writing new FreeRADIUS SQL module" - SQL_FILE="/etc/freeradius/mods-available/sql" - -# Backup if it exists -[ -f "$SQL_FILE" ] && cp "$SQL_FILE" "${SQL_FILE}.bak.$(date +%Y%m%d-%H%M%S)" - cat > "$SQL_FILE" < /etc/supervisor/conf.d/queue-worker.conf << "EOL" -[program:queue-worker] -process_name=%(program_name)s_%(process_num)02d -command=php /var/www/html/artisan queue:work --tries=3 -autostart=true -autorestart=true -stopasgroup=true -killasgroup=true -user=www-data -numprocs=5 -redirect_stderr=true -stdout_logfile=/var/www/html/storage/logs/queue-worker.log -EOL -COMPLETED_STEPS+=("Supervisor configured for queue worker") - -# Install OpenVPN based on Ubuntu version -log_step "Installing OpenVPN" -case $UBUNTU_VERSION in - "focal"|"jammy"|"noble") - echo "Installing OpenVPN for Ubuntu $UBUNTU_VERSION" - export AUTO_INSTALL=y - curl -O https://raw.githubusercontent.com/simpleisp/bash/main/openvpn.sh || handle_error "Failed to download OpenVPN installer" - chmod +x openvpn.sh || handle_error "Failed to make OpenVPN installer executable" - ./openvpn.sh || handle_error "Failed to install OpenVPN" - - # Enable and start OpenVPN service - systemctl enable openvpn || handle_error "Failed to enable OpenVPN service" - systemctl start openvpn || handle_error "Failed to start OpenVPN service" - - # Set more secure permissions for OpenVPN - chown -R root:root /etc/openvpn || handle_error "Failed to set ownership of OpenVPN configuration directory" - chmod -R 777 /etc/openvpn || handle_error "Failed to set permissions of OpenVPN configuration directory" - chmod -R 777 /etc/openvpn/easy-rsa || handle_error "Failed to set permissions of OpenVPN easy-rsa directory" - ;; - *) - handle_error "Unsupported Ubuntu version for OpenVPN installation: $UBUNTU_VERSION" - ;; -esac -COMPLETED_STEPS+=("OpenVPN installed") - -# Configure Nginx -log_step "Configuring Nginx" -cat > /etc/nginx/sites-available/default << EOL -server { - - listen 80; - listen [::]:80; - - root /var/www/html/public; - index index.php index.html index.htm index.nginx-debian.html; - - server_name $DOMAIN; - - location / { - try_files \$uri \$uri/ /index.php?\$query_string; - } - - location ~ \.php$ { - include snippets/fastcgi-php.conf; - fastcgi_pass unix:/var/run/php/php${PHP_VERSION}-fpm.sock; - } - - location ~ /\.ht { - deny all; - } - + sed -i 's/-sql/sql/g' "$DEFAULT_SITE" || handle_error "Failed to update -sql to sql" + sed -i 's/^[[:space:]]*detail/# detail/' "$DEFAULT_SITE" || handle_error "Failed to comment out detail line" + log_step "Replacing accounting section in FreeRADIUS default site" + cat << 'EOF' > /tmp/new_accounting_block +accounting { +sql +exec +attr_filter.accounting_response } -EOL -COMPLETED_STEPS+=("Nginx configured") - -# Enable the default site -log_step "Enabling Nginx default site" -ln -sf /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default || handle_error "Failed to enable Nginx default site" -COMPLETED_STEPS+=("Nginx default site enabled") - -# Test the Nginx configuration -log_step "Testing Nginx configuration" -nginx -t || handle_error "Failed to test Nginx configuration" -COMPLETED_STEPS+=("Nginx configuration tested") - -# If the configuration is OK, reload and restart Nginx -log_step "Reloading and restarting Nginx" -systemctl reload nginx || handle_error "Failed to reload Nginx" -systemctl restart nginx || handle_error "Failed to restart Nginx" -COMPLETED_STEPS+=("Nginx reloaded and restarted") - -# Set correct permissions -log_step "Setting correct permissions" -chown -R www-data:www-data /var/www/html || handle_error "Failed to set ownership of web root" -chmod -R 775 /var/www/html/storage || handle_error "Failed to set permissions of storage directory" -chmod -R 775 /var/www/html/bootstrap/cache || handle_error "Failed to set permissions of cache directory" - -# Make the scripts executable -chmod +x /var/www/html/sh/set_permissions.sh || handle_error "Failed to make permissions script executable" -chmod +x /var/www/html/sh/restart-services.sh || handle_error "Failed to make services script executable" - -# Run the script once to apply initial configuration -/var/www/html/sh/set_permissions.sh || handle_error "Failed to run permissions script" -/var/www/html/sh/restart-services.sh || handle_error "Failed to run services script" - -COMPLETED_STEPS+=("Correct permissions set") - -# Install cron -log_step "Installing cron" -# Write cron job entry to a temporary file -echo "* * * * * php /var/www/html/artisan schedule:run >> /dev/null 2>&1" > cronjob || handle_error "Failed to write cron job to temporary file" - -# Install the cron job from the temporary file -crontab cronjob || handle_error "Failed to install cron job" -COMPLETED_STEPS+=("Cron job installed") - -# Clean up the temporary file -rm cronjob || handle_error "Failed to remove temporary cron job file" -COMPLETED_STEPS+=("Temporary cron job file removed") - -# Update sudoers for www-data user -log_step "Updating sudoers for www-data user" -cat >> /etc/sudoers << "EOL" -www-data ALL=NOPASSWD: /bin/systemctl start openvpn -www-data ALL=NOPASSWD: /bin/systemctl stop openvpn -www-data ALL=NOPASSWD: /bin/systemctl restart openvpn -www-data ALL=NOPASSWD: /bin/systemctl status openvpn -www-data ALL=NOPASSWD: /bin/systemctl reload openvpn -www-data ALL=NOPASSWD: /bin/systemctl enable openvpn -www-data ALL=NOPASSWD: /bin/systemctl disable openvpn -www-data ALL=NOPASSWD: /bin/systemctl start freeradius -www-data ALL=NOPASSWD: /bin/systemctl stop freeradius -www-data ALL=NOPASSWD: /bin/systemctl restart freeradius -www-data ALL=NOPASSWD: /bin/systemctl status freeradius -www-data ALL=NOPASSWD: /bin/systemctl reload freeradius -www-data ALL=NOPASSWD: /bin/systemctl enable freeradius -www-data ALL=NOPASSWD: /bin/systemctl disable freeradius -www-data ALL=NOPASSWD: /bin/supervisorctl stop all -www-data ALL=NOPASSWD: /bin/supervisorctl reread -www-data ALL=NOPASSWD: /bin/supervisorctl update -www-data ALL=NOPASSWD: /bin/supervisorctl start all -www-data ALL=NOPASSWD: /bin/supervisorctl restart all -www-data ALL=NOPASSWD: /bin/supervisorctl status -www-data ALL=NOPASSWD: /bin/systemctl restart supervisor -www-data ALL=NOPASSWD: /bin/systemctl status ssh -www-data ALL=NOPASSWD: /var/www/html/sh/set_permissions.sh -www-data ALL=NOPASSWD: /var/www/html/sh/restart-services.sh -EOL -COMPLETED_STEPS+=("Sudoers updated for www-data user") - -# Save database credentials -log_step "Saving database credentials" -echo "MySQL Credentials:" > "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" -echo "DB_HOST=localhost" >> "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" -echo "DB_PORT=3306" >> "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" -echo "DB_DATABASE=$MYSQL_DATABASE" >> "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" -echo "DB_USERNAME=$MYSQL_USER" >> "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" -echo "DB_PASSWORD=$MYSQL_PASSWORD" >> "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" -COMPLETED_STEPS+=("Database credentials saved") - -# Start and enable all services -log_step "Starting and enabling all services" -systemctl start nginx || handle_error "Failed to start Nginx" -systemctl enable nginx || handle_error "Failed to enable Nginx" -systemctl start php${PHP_VERSION}-fpm || handle_error "Failed to start PHP ${PHP_VERSION} FPM" -systemctl enable php${PHP_VERSION}-fpm || handle_error "Failed to enable PHP ${PHP_VERSION} FPM" -systemctl start supervisor || handle_error "Failed to start Supervisor" -systemctl enable supervisor || handle_error "Failed to enable Supervisor" -systemctl start openvpn || handle_error "Failed to start OpenVPN" -systemctl enable openvpn || handle_error "Failed to enable OpenVPN" -COMPLETED_STEPS+=("Core services started and enabled") - -# Restart all services to ensure proper configuration -log_step "Restarting all services" -systemctl restart nginx || handle_error "Failed to restart Nginx" -systemctl restart php${PHP_VERSION}-fpm || handle_error "Failed to restart PHP ${PHP_VERSION} FPM" -systemctl restart supervisor || handle_error "Failed to restart Supervisor" -systemctl restart openvpn || handle_error "Failed to restart OpenVPN" -COMPLETED_STEPS+=("Services restarted") - -# Open Firewall Ports and enable ufw -log_step "Opening firewall ports and enabling ufw" -ufw allow ssh || handle_error "Failed to allow SSH through firewall" -ufw allow 9080/tcp || handle_error "Failed to allow port 9080 through firewall" -ufw allow http || handle_error "Failed to allow HTTP through firewall" -ufw allow https || handle_error "Failed to allow HTTPS through firewall" -ufw allow 1194/tcp || handle_error "Failed to allow OpenVPN through firewall" -ufw allow 1812:1813/udp || handle_error "Failed to allow FreeRADIUS through firewall" -ufw reload || handle_error "Failed to reload firewall rules" -yes | ufw enable || handle_error "Failed to enable firewall" -COMPLETED_STEPS+=("Firewall ports opened and ufw enabled") - -# Configure SSL with Certbot -log_step "Configuring SSL with Certbot" -echo "Configuring SSL certificate for $DOMAIN" - -# Check if SSL certificate already exists for this domain -if [ -d "/etc/letsencrypt/live/$DOMAIN" ]; then - log_info "SSL certificate already exists for $DOMAIN, configuring it in Nginx" - # Configure existing certificate in Nginx - certbot --nginx -d "$DOMAIN" --agree-tos --email "$EMAIL_ADDRESS" --no-eff-email --non-interactive --redirect || handle_error "Failed to configure existing SSL certificate in Nginx" - COMPLETED_STEPS+=("SSL certificate configured in Nginx (existing certificate)") +EOF + awk ' + BEGIN { skip = 0 } + /^accounting[ \t]*{/ { print_block = 1; print_file("/tmp/new_accounting_block"); skip = 1; next } + /^[ \t]*}/ { if (skip) { skip = 0; next } } + !skip { print } + function print_file(file) { + while ((getline line < file) > 0) print line; + close(file) + } + ' "$DEFAULT_SITE" > /tmp/tmp_site && mv /tmp/tmp_site "$DEFAULT_SITE" else - log_info "No existing SSL certificate found for $DOMAIN, requesting new certificate" - certbot --nginx -d "$DOMAIN" --agree-tos --email "$EMAIL_ADDRESS" --no-eff-email --non-interactive --redirect || handle_error "Failed to configure SSL with Certbot" - COMPLETED_STEPS+=("SSL configured with Certbot") + handle_error "Default site configuration file not found" fi +COMPLETED_STEPS+=("FreeRADIUS default site configured") -# Create cleanup script for uninstalling all software -log_step "Creating cleanup script" -cat > /root/clean_server.sh << 'EOL' -#!/bin/bash - -# Cleanup script for SimpleISP/SimpleSpot -# This script will uninstall all software installed by the SimpleISP/SimpleSpot installer -# and clean the server for reinstallation - -echo "[$(date)] Starting cleanup process..." - -# Function to handle errors -handle_error() { - echo "[$(date)] ERROR: $1" - exit 1 -} +# Apply Systemd Sandbox changes from the overrides +systemctl daemon-reload || handle_error "Failed to reload systemd daemon" -# Function to log steps -log_step() { - echo "[$(date)] STEP: $1" -} +# Enable and start all services (Consolidated Block) +log_step "Enabling and restarting all services" -# Confirm before proceeding -echo "WARNING: This will remove ALL software installed by SimpleISP/SimpleSpot and delete all data." -echo "This action CANNOT be undone!" -read -p "Are you sure you want to proceed? (y/n): " -n 1 -r -echo -if [[ ! $REPLY =~ ^[Yy]$ ]]; then - echo "Cleanup cancelled." - exit 0 -fi +SERVICES=(nginx php${PHP_VERSION}-fpm supervisor openvpn@server freeradius valkey-server) +for service in "${SERVICES[@]}"; do + systemctl enable "$service" || handle_error "Failed to enable $service" + systemctl restart "$service" || handle_error "Failed to restart $service" +done +COMPLETED_STEPS+=("All services enabled and restarted") -# Detect PHP version (try common versions) -PHP_VERSION="" -for version in 8.2 8.1 8.0 7.4; do - if command -v php${version} &> /dev/null; then - PHP_VERSION=$version - break +# Install maintenance scripts (autotune, DB cleanup, OpenVPN sandbox fix) +log_step "Installing maintenance scripts to /var/www/html/sh" +for s in universal.sh db_cleanup.sh ovpn_fix.sh; do + curl -fsSL "https://raw.githubusercontent.com/simpleisp/bash/main/${s}" -o "/var/www/html/sh/${s}" || handle_error "Failed to download ${s}" + chmod +x "/var/www/html/sh/${s}" || handle_error "Failed to make ${s} executable" +done +COMPLETED_STEPS+=("Maintenance scripts installed to /var/www/html/sh") + +# Schedule maintenance (Safe Append Fix): autotune daily at 3 AM and on every +# reboot (delayed so MariaDB/Valkey/PHP-FPM are up before live tuning starts); +# DB cleanup at 04:30 (non-interactive runs use its built-in retention defaults) +log_step "Scheduling maintenance cron jobs" +(crontab -l 2>/dev/null; echo "0 3 * * * /var/www/html/sh/universal.sh") | crontab - || handle_error "Failed to add universal.sh daily cron job" +(crontab -l 2>/dev/null; echo "@reboot sleep 120; /var/www/html/sh/universal.sh") | crontab - || handle_error "Failed to add universal.sh reboot cron job" +(crontab -l 2>/dev/null; echo "30 4 * * * /var/www/html/sh/db_cleanup.sh") | crontab - || handle_error "Failed to add db_cleanup.sh cron job" +COMPLETED_STEPS+=("Maintenance cron jobs scheduled (universal.sh 3AM + reboot, db_cleanup.sh 04:30)") + +# Run the autotune once to apply the initial configuration +log_step "Running initial system autotune (universal.sh)" +/var/www/html/sh/universal.sh || handle_error "Failed to run initial autotune (universal.sh)" +COMPLETED_STEPS+=("Initial autotune applied (universal.sh)") + +# Verify PHP-FPM/queue workers can write /etc/openvpn despite systemd sandboxing. +# Idempotent: no-ops where the static overrides above already grant access, and +# catches any additional php-fpm versions or queue-worker units. +log_step "Verifying OpenVPN write access for PHP-FPM (ovpn_fix.sh)" +/var/www/html/sh/ovpn_fix.sh || log_warning "ovpn_fix.sh reported issues; review the output above and re-run /var/www/html/sh/ovpn_fix.sh manually" +COMPLETED_STEPS+=("OpenVPN sandbox write access verified (ovpn_fix.sh)") + +# Final verification +log_step "Verifying all services are running" +for service in nginx mariadb freeradius valkey-server php${PHP_VERSION}-fpm; do + if ! systemctl is-active --quiet $service; then + log_warning "$service is not running" + systemctl status $service + else + log_success "$service is running" fi done -if [ -z "$PHP_VERSION" ]; then - echo "Warning: Could not detect PHP version, using 8.2 as default" - PHP_VERSION="8.2" -fi - -echo "[$(date)] Detected PHP version: $PHP_VERSION" - -# Stop services -log_step "Stopping services" -systemctl stop nginx freeradius mariadb valkey php${PHP_VERSION}-fpm supervisor openvpn || echo "Could not stop all services" - -# Remove web files -log_step "Removing web files" -rm -rf /var/www/html/* 2>/dev/null -rm -rf /var/www/html/.* 2>/dev/null - -# Remove configuration directories -log_step "Removing configuration directories" -rm -rf /etc/nginx/sites-available/default 2>/dev/null -rm -rf /etc/nginx/sites-enabled/default 2>/dev/null -rm -rf /etc/freeradius 2>/dev/null -rm -rf /etc/openvpn 2>/dev/null -rm -rf /etc/supervisor 2>/dev/null - -# Remove Redis data -log_step "Removing Redis data" -rm -rf /var/lib/redis/* 2>/dev/null -rm -rf /var/lib/redis/.* 2>/dev/null - -# Remove MySQL/MariaDB data and users -log_step "Removing MySQL/MariaDB data and users" -systemctl stop mariadb 2>/dev/null || echo "MariaDB was not running" -mysql -e "DROP USER IF EXISTS 'simpleisp'@'%';" 2>/dev/null || echo "Could not remove simpleisp user" -mysql -e "DROP USER IF EXISTS 'radius'@'%';" 2>/dev/null || echo "Could not remove radius user" -mysql -e "DROP DATABASE IF EXISTS radius;" 2>/dev/null || echo "Could not remove radius database" -mysql -e "FLUSH PRIVILEGES;" 2>/dev/null - -# Remove MySQL/MariaDB files -log_step "Removing MySQL/MariaDB files" -systemctl stop mariadb 2>/dev/null || echo "MariaDB already stopped" -rm -rf /var/lib/mysql 2>/dev/null -mkdir -p /var/lib/mysql -chown mysql:mysql /var/lib/mysql -rm -rf /run/mysqld 2>/dev/null -rm -f /root/.my.cnf 2>/dev/null -rm -f /root/.mysql_history 2>/dev/null - -# Remove log files -log_step "Removing log files" -rm -f /var/log/nginx/access.log 2>/dev/null -rm -f /var/log/nginx/error.log 2>/dev/null -rm -f /var/log/freeradius/radius.log 2>/dev/null - -# Remove SSL certificates -log_step "Preserving SSL certificates (not removing for reuse on reinstall)" -# rm -rf /etc/letsencrypt/live/* 2>/dev/null -# rm -rf /etc/letsencrypt/archive/* 2>/dev/null -# rm -rf /etc/letsencrypt/renewal/* 2>/dev/null - -# Remove application-specific files -log_step "Removing application-specific files" -# Preserve db.txt for credential reuse on reinstall -# rm -f /root/db.txt 2>/dev/null -rm -f /etc/cron.d/laravel-scheduler 2>/dev/null - -# Remove ionCube files -log_step "Removing ionCube files" -rm -f /etc/php/${PHP_VERSION}/mods-available/ioncube.ini 2>/dev/null -rm -f /etc/php/${PHP_VERSION}/cli/conf.d/00-ioncube.ini 2>/dev/null -rm -f /etc/php/${PHP_VERSION}/fpm/conf.d/00-ioncube.ini 2>/dev/null -# Preserve ionCube installation for reuse on reinstall -# rm -rf /usr/local/ioncube 2>/dev/null -find /usr/lib/php/ -name "*ioncube*" -delete 2>/dev/null - -# Remove temporary files -log_step "Removing temporary files" -rm -rf /tmp/ioncube* 2>/dev/null -rm -f /tmp/*.zip 2>/dev/null -rm -f /tmp/*.tar.gz 2>/dev/null - -# Create cleanup marker -log_step "Creating cleanup marker" -date '+%Y-%m-%d %H:%M:%S' > "/root/.simpleisp_cleanup_done" -echo "[$(date)] Cleanup completed. Marker created at /root/.simpleisp_cleanup_done" - -echo "[$(date)] Cleanup completed. The server is now clean and ready for reinstallation." -EOL - -# Make the cleanup script executable -chmod +x /root/clean_server.sh || handle_error "Failed to make cleanup script executable" -log_success "Cleanup script created at /root/clean_server.sh" -COMPLETED_STEPS+=("Cleanup script created at /root/clean_server.sh") - +# Complete installation message log_success "Installation completed successfully!" echo "You can find your database credentials in $DB_CREDENTIALS_FILE" echo "Your SimpleISP installation is available at: https://$DOMAIN" -echo "Installation logs are available at: $INSTALL_LOG" -echo "To uninstall everything and clean the server, run: /root/clean_server.sh" diff --git a/simpleisp.sh b/ubuntu_simpleisp_old.sh similarity index 56% rename from simpleisp.sh rename to ubuntu_simpleisp_old.sh index 240ba66..91aa1d6 100644 --- a/simpleisp.sh +++ b/ubuntu_simpleisp_old.sh @@ -1,23 +1,31 @@ -#!/bin/bash +#!/bin/bash # Setup logging and error handling INSTALL_LOG="/root/install.txt" STEP_COUNT=0 COMPLETED_STEPS=() -# Get server hostname and set email -DOMAIN=$(hostname -f) -EMAIL_ADDRESS="simpluxsolutions@gmail.com" - # Configuration variables - ONLY DIFFERENCES BETWEEN SCRIPTS GITHUB_REPO_URL="https://github.com/simpleisp/radius.git" PHP_VERSION="7.4" # Logging functions -log_info() { echo "ℹ️ INFO: $1" | tee -a "$INSTALL_LOG"; } -log_success() { echo "✅ SUCCESS: $1" | tee -a "$INSTALL_LOG"; } -log_error() { echo "❌ ERROR: $1" | tee -a "$INSTALL_LOG"; } -log_step() { STEP_COUNT=$((STEP_COUNT + 1)); echo "👉 STEP $STEP_COUNT: $1" | tee -a "$INSTALL_LOG"; } +log_info() { + echo "ℹ️ INFO: $1" | tee -a "$INSTALL_LOG" +} + +log_success() { + echo "✅ SUCCESS: $1" | tee -a "$INSTALL_LOG" +} + +log_error() { + echo "❌ ERROR: $1" | tee -a "$INSTALL_LOG" +} + +log_step() { + STEP_COUNT=$((STEP_COUNT + 1)) + echo "👉 STEP $STEP_COUNT: $1" | tee -a "$INSTALL_LOG" +} handle_error() { log_error "$1" @@ -53,7 +61,7 @@ if [ -f "$CLEANUP_MARKER" ]; then log_info "Detected previous cleanup ($(cat $CLEANUP_MARKER))" log_info "Forcing reinstallation of critical directories and files" REINSTALL=true - + # Remove the marker file after handling it rm -f "$CLEANUP_MARKER" log_success "Cleanup marker processed and removed" @@ -61,9 +69,13 @@ fi # Ensure script runs as root log_step "Checking root privileges" -if [ "$EUID" -ne 0 ]; then handle_error "Please run as root"; fi +if [ "$EUID" -ne 0 ]; then + handle_error "Please run as root" +fi COMPLETED_STEPS+=("Root check passed") +# Set environment variable to avoid interactive prompts +export DEBIAN_FRONTEND=noninteractive # Get Ubuntu version log_step "Detecting Ubuntu version" UBUNTU_VERSION=$(lsb_release -cs) || handle_error "Failed to detect Ubuntu version" @@ -86,7 +98,7 @@ if [ "$UBUNTU_VERSION" = "noble" ]; then gpg --dry-run --quiet --import --import-options import-show ${gpgKeyPath} # Create the sources file for PHP repository - cat > /etc/apt/sources.list.d/ondrej-ubuntu-php-noble.sources << 'EOL' + cat > /etc/apt/sources.list.d/ondrej-ubuntu-php-noble.sources << "EOL" Types: deb URIs: https://ppa.launchpadcontent.net/ondrej/php/ubuntu/ Suites: noble @@ -110,18 +122,46 @@ printf 'Package: /freeradius/\nPin: origin "packages.networkradius.com"\nPin-Pri # Add NetworkRADIUS repository based on Ubuntu version case $UBUNTU_VERSION in - "noble") REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/noble noble main" ;; - "jammy") REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/jammy jammy main" ;; - "focal") REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/focal focal main" ;; - *) handle_error "Unsupported Ubuntu version: $UBUNTU_VERSION" ;; + "noble") + REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/noble noble main" + ;; + "jammy") + REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/jammy jammy main" + ;; + "focal") + REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/focal focal main" + ;; + *) + handle_error "Unsupported Ubuntu version: $UBUNTU_VERSION" + ;; esac echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/packages.networkradius.com.asc] $REPO_URL" | sudo tee /etc/apt/sources.list.d/networkradius.list > /dev/null || handle_error "Failed to add NetworkRADIUS repository" log_success "NetworkRADIUS repository configured for Ubuntu $UBUNTU_VERSION" COMPLETED_STEPS+=("NetworkRADIUS repository configured") -# Set environment variable to avoid interactive prompts -export DEBIAN_FRONTEND=noninteractive +# Set Valkey Repository +log_step "Adding Valkey repository" +if [[ "$UBUNTU_VERSION" == "focal" || "$UBUNTU_VERSION" == "jammy" ]]; then + # Remove conflicting redis packages + apt-get remove -y redis-tools redis-server || true + + # Fetch Percona release package + wget https://repo.percona.com/apt/percona-release_latest.$(lsb_release -sc)_all.deb + + # Install Percona release package + dpkg -i percona-release_latest.$(lsb_release -sc)_all.deb + + # Enable Percona repository for Valkey + percona-release enable valkey experimental + + # Update package list + apt-get update +fi + +COMPLETED_STEPS+=("Valkey repository added") + + # Update and upgrade system log_step "Updating system packages" @@ -129,52 +169,105 @@ apt-get update || handle_error "Failed to update package lists" apt-get upgrade -y || handle_error "Failed to upgrade packages" COMPLETED_STEPS+=("System packages updated") -# Install required packages (Cleaned virtual PHP packages and freeradius-rest) +# Install required packages log_step "Installing required packages" -INSTALL_PACKAGES="nginx-full python3-certbot-nginx php${PHP_VERSION}-fpm php${PHP_VERSION}-mysql php${PHP_VERSION}-cli php${PHP_VERSION}-curl php${PHP_VERSION}-zip php${PHP_VERSION}-common php${PHP_VERSION}-gd php${PHP_VERSION}-mbstring php${PHP_VERSION}-xml php${PHP_VERSION}-dev php${PHP_VERSION}-bcmath php${PHP_VERSION}-intl php${PHP_VERSION}-redis git unzip curl wget software-properties-common apt-transport-https ca-certificates gnupg lsb-release supervisor valkey-server valkey-tools valkey-redis-compat valkey-sentinel ufw openvpn easy-rsa freeradius freeradius-mysql freeradius-utils mariadb-server mariadb-client" - if [ "$REINSTALL" = true ]; then log_info "Reinstalling packages (forcing configuration file replacement)" - apt-get install --reinstall -y -o Dpkg::Options::="--force-confmiss" $INSTALL_PACKAGES || handle_error "Failed to reinstall packages" + apt-get install --reinstall -y -o Dpkg::Options::="--force-confmiss" \ + nginx-full \ + python3-certbot-nginx \ + php${PHP_VERSION}-fpm \ + php${PHP_VERSION}-mysql \ + php${PHP_VERSION}-cli \ + php${PHP_VERSION}-curl \ + php${PHP_VERSION}-zip \ + php${PHP_VERSION}-common \ + php${PHP_VERSION}-gd \ + php${PHP_VERSION}-mbstring \ + php${PHP_VERSION}-xml \ + php${PHP_VERSION}-dev \ + php${PHP_VERSION}-bcmath \ + php${PHP_VERSION}-intl \ + php${PHP_VERSION}-redis \ + php${PHP_VERSION}-tokenizer \ + php${PHP_VERSION}-ctype \ + php${PHP_VERSION}-fileinfo \ + php${PHP_VERSION}-json \ + git \ + unzip \ + curl \ + wget \ + software-properties-common \ + apt-transport-https \ + ca-certificates \ + gnupg \ + lsb-release \ + supervisor \ + valkey \ + valkey-compat \ + openvpn \ + easy-rsa \ + freeradius \ + freeradius-mysql \ + freeradius-utils \ + mariadb-server \ + mariadb-client || handle_error "Failed to reinstall packages" else - apt-get install -y $INSTALL_PACKAGES || handle_error "Failed to install packages" + apt-get install -y \ + nginx-full \ + python3-certbot-nginx \ + php${PHP_VERSION}-fpm \ + php${PHP_VERSION}-mysql \ + php${PHP_VERSION}-cli \ + php${PHP_VERSION}-curl \ + php${PHP_VERSION}-zip \ + php${PHP_VERSION}-common \ + php${PHP_VERSION}-gd \ + php${PHP_VERSION}-mbstring \ + php${PHP_VERSION}-xml \ + php${PHP_VERSION}-dev \ + php${PHP_VERSION}-bcmath \ + php${PHP_VERSION}-intl \ + php${PHP_VERSION}-redis \ + php${PHP_VERSION}-tokenizer \ + php${PHP_VERSION}-ctype \ + php${PHP_VERSION}-fileinfo \ + php${PHP_VERSION}-json \ + git \ + unzip \ + curl \ + wget \ + software-properties-common \ + apt-transport-https \ + ca-certificates \ + gnupg \ + lsb-release \ + supervisor \ + valkey \ + valkey-compat \ + openvpn \ + easy-rsa \ + freeradius \ + freeradius-mysql \ + freeradius-utils \ + mariadb-server \ + mariadb-client || handle_error "Failed to install packages" fi COMPLETED_STEPS+=("Required packages installed") -# Configure Valkey service overrides -log_step "Configuring Valkey service overrides" -VKEY_OVERRIDE_DIR="/etc/systemd/system/valkey.service.d" -VKEY_OVERRIDE_FILE="${VKEY_OVERRIDE_DIR}/override.conf" - -mkdir -p "$VKEY_OVERRIDE_DIR" -cat > "$VKEY_OVERRIDE_FILE" << 'EOF' -[Unit] - -[Service] -# Increase timeouts to prevent premature termination -TimeoutStartSec=300 -TimeoutStopSec=300 - -# Ensure service restarts on failure -Restart=always -RestartSec=10s - -# Disable OOM killer for Valkey -OOMScoreAdjust=-1000 -EOF - -# Apply changes and restart Valkey -log_info "Applying Valkey service configuration..." - -# Configure Valkey with optimized settings for FreeRADIUS +# Configure Valkey with optimal settings for FreeRADIUS log_step "Configuring Valkey with optimized settings" -# Calculate optimal memory allocation (Safe 15% of available RAM, capped at 3GB, minimum 1GB) +# Calculate optimal memory allocation (75% of available RAM, capped at 3GB, minimum 1GB) TOTAL_RAM_KB=$(grep MemTotal /proc/meminfo | awk '{print $2}') TOTAL_RAM_MB=$((TOTAL_RAM_KB / 1024)) -MAX_MEMORY_MB=$((TOTAL_RAM_MB * 15 / 100)) -if [ "$MAX_MEMORY_MB" -gt 3072 ]; then MAX_MEMORY_MB=3072; fi -if [ "$MAX_MEMORY_MB" -lt 1024 ]; then MAX_MEMORY_MB=1024; fi +MAX_MEMORY_MB=$((TOTAL_RAM_MB * 75 / 100)) +if [ "$MAX_MEMORY_MB" -gt 3072 ]; then + MAX_MEMORY_MB=3072 # Cap at 3GB +fi +if [ "$MAX_MEMORY_MB" -lt 1024 ]; then + MAX_MEMORY_MB=1024 # Minimum 1GB +fi log_info "Configuring Valkey with ${MAX_MEMORY_MB}MB memory allocation" @@ -184,6 +277,7 @@ mkdir -p /etc/valkey || handle_error "Failed to create Valkey configuration dire # Configure Valkey with optimized settings for FreeRADIUS cat > /etc/valkey/valkey.conf << EOL # Valkey configuration for FreeRADIUS + bind 0.0.0.0 ::0 protected-mode yes port 6379 @@ -222,7 +316,8 @@ dbfilename dump.rdb # Disable RDB snapshots since we're using AOF save "" -# Security (Reuse the same password as MySQL for simplicity) +# Security +# Reuse the same password as MySQL for simplicity requirepass "$MYSQL_PASSWORD" # Network @@ -231,6 +326,8 @@ repl-timeout 60 repl-ping-slave-period 10 repl-backlog-size 1mb repl-backlog-ttl 3600 + +# Client timeouts timeout 0 tcp-keepalive 300 @@ -240,23 +337,39 @@ rename-command FLUSHALL "" rename-command CONFIG "" rename-command SHUTDOWN "" -# Tune threads and clients +# Set the number of threads to serve the requests io-threads 2 io-threads-do-reads yes + +# Set the max number of connected clients at the same time maxclients 10000 -# Tune data structures +# Set the threshold for keys with an expire set to be considered for deletion active-expire-effort 1 + +# Set the threshold for client output buffer limits client-output-buffer-limit normal 0 0 0 client-output-buffer-limit replica 256mb 64mb 60 client-output-buffer-limit pubsub 32mb 8mb 60 + +# Tune hash data structure hash-max-ziplist-entries 512 hash-max-ziplist-value 64 + +# Tune list data structure list-max-ziplist-size -2 + +# Tune set data structure set-max-intset-entries 512 + +# Tune zset data structure zset-max-ziplist-entries 128 zset-max-ziplist-value 64 + +# Tune hll data structure hll-sparse-max-bytes 3000 + +# Tune stream data structure stream-node-max-bytes 4096 stream-node-max-entries 100 @@ -269,27 +382,41 @@ active-defrag-cycle-max 75 active-defrag-max-scan-fields 1000 EOL +# Set proper permissions for Valkey directories +log_step "Setting Valkey directory permissions" +mkdir -p /var/lib/valkey/appendonlydir +chown -R valkey:valkey /var/lib/valkey /var/log/valkey /var/run/valkey +chmod 750 /var/lib/valkey /var/log/valkey /var/run/valkey + +# Fix Valkey service +sed -i 's/ConditionPathExists=!\/etc\/valkey\/REDIS_MIGRATION/ConditionPathExists=\/etc\/valkey\/REDIS_MIGRATION/g' /usr/lib/systemd/system/valkey.service + # Restart Valkey to apply new configuration systemctl daemon-reexec || log_warning "daemon-reexec failed (non-critical)" systemctl daemon-reload || handle_error "Failed to reload systemd daemon" -systemctl restart valkey-server || handle_error "Failed to restart Valkey" -systemctl enable valkey-server || handle_error "Failed to enable Valkey" +systemctl restart valkey || handle_error "Failed to restart Valkey" +systemctl enable valkey || handle_error "Failed to enable Valkey" # Verify Valkey is running log_step "Verifying Valkey service status" -if systemctl is-active --quiet valkey-server; then + +# Check service status +if systemctl is-active --quiet valkey; then log_success "Valkey service is running" COMPLETED_STEPS+=("Valkey configured with ${MAX_MEMORY_MB}MB memory allocation") else + # If service is not running, try to get more information log_warning "Valkey service is not running as expected. Checking status..." - systemctl status valkey-server --no-pager || true + systemctl status valkey --no-pager || true + # Try to start the service log_info "Attempting to start Valkey service..." - if systemctl start valkey-server; then + if systemctl start valkey; then log_success "Successfully started Valkey service" COMPLETED_STEPS+=("Valkey configured with ${MAX_MEMORY_MB}MB memory allocation") else - log_error "Failed to start Valkey service. Please check the logs with: journalctl -u valkey-server -n 50" + # If we still can't start, show detailed error but don't fail the script + log_error "Failed to start Valkey service. Please check the logs with: journalctl -u valkey -n 50" log_warning "Continuing installation despite Valkey service issue..." COMPLETED_STEPS+=("Valkey configuration completed but service failed to start") fi @@ -308,26 +435,52 @@ systemctl status valkey --no-pager -l echo -e "\n=== Valkey Key Statistics ===" echo "Total Keys in DB 0: $(valkey-cli -h $VALKEY_HOST -p $VALKEY_PORT dbsize)" + EOF -chmod +x /usr/local/bin/valkey-debug.sh || handle_error "Failed to make Valkey debug script executable" +chmod +x /usr/local/bin/valkey-debug.sh + COMPLETED_STEPS+=("Valkey monitoring configured") -# Add monitoring cron job (Safe Append Fix) +# Add monitoring cron job log_step "Adding monitoring cron job" -(crontab -l 2>/dev/null; echo "*/5 * * * * /usr/local/bin/valkey-debug.sh") | crontab - || handle_error "Failed to install monitoring cron job" +echo "*/5 * * * * /usr/local/bin/valkey-debug.sh" > cronjob || handle_error "Failed to write monitoring cron job to temporary file" +crontab cronjob || handle_error "Failed to install monitoring cron job" +rm cronjob || handle_error "Failed to remove temporary monitoring cron job file" COMPLETED_STEPS+=("Monitoring cron job added") # Verify Valkey is working log_step "Verifying Valkey installation" -if ! systemctl is-active --quiet valkey; then handle_error "Valkey service is not running"; fi +if ! systemctl is-active --quiet valkey; then +handle_error "Valkey service is not running" +fi # Test Valkey connectivity and basic operations -if [ "$(valkey-cli ping)" != "PONG" ]; then handle_error "Valkey is not responding to ping"; fi -if [ "$(valkey-cli set test_key test_value)" != "OK" ]; then handle_error "Valkey write operation failed"; fi -if [ "$(valkey-cli get test_key)" != "test_value" ]; then handle_error "Valkey read operation failed"; fi -if [ "$(valkey-cli del test_key)" != "1" ]; then handle_error "Valkey delete operation failed"; fi -if ! valkey-cli info | grep -q "valkey_version"; then handle_error "Unable to get Valkey server information"; fi +if [ "$(valkey-cli ping)" != "PONG" ]; then +handle_error "Valkey is not responding to ping" +fi + +# Test Valkey write operation +if [ "$(valkey-cli set test_key test_value)" != "OK" ]; then +handle_error "Valkey write operation failed" +fi + +# Test Valkey read operation +TEST_VALUE=$(valkey-cli get test_key) +if [ "$TEST_VALUE" != "test_value" ]; then +handle_error "Valkey read operation failed" +fi + +# Test Valkey delete operation +if [ "$(valkey-cli del test_key)" != "1" ]; then +handle_error "Valkey delete operation failed" +fi + +# Check Valkey info for basic stats +if ! valkey-cli info | grep -q "valkey_version"; then +handle_error "Unable to get Valkey server information" +fi + COMPLETED_STEPS+=("Valkey functionality verified") # Set Default PHP Version @@ -337,16 +490,27 @@ COMPLETED_STEPS+=("PHP ${PHP_VERSION} set as default") # Install and configure ionCube Loader log_step "Installing ionCube Loader" + +# Check if ionCube is already installed if [ -d "/usr/local/ioncube" ] && [ -f "/usr/local/ioncube/ioncube_loader_lin_${PHP_VERSION}.so" ]; then log_info "ionCube Loader already exists, skipping download and installation" COMPLETED_STEPS+=("ionCube Loader reused (already exists)") else log_info "ionCube Loader not found, downloading and installing" + + # Change to /tmp directory for downloads cd /tmp || handle_error "Failed to change to /tmp directory" + + # Download and extract ionCube wget -O ioncube.zip "https://downloads.ioncube.com/loader_downloads/ioncube_loaders_lin_x86-64.zip" || handle_error "Failed to download ionCube" unzip -q ioncube.zip || handle_error "Failed to extract ionCube" + + # Remove existing ionCube directory if it exists rm -rf /usr/local/ioncube 2>/dev/null + + # Move the ioncube directory to /usr/local mv ioncube /usr/local/ || handle_error "Failed to move ionCube to /usr/local" + COMPLETED_STEPS+=("ionCube Loader downloaded and installed") fi @@ -358,6 +522,8 @@ EOL # Enable ionCube for PHP CLI and FPM ln -sf /etc/php/${PHP_VERSION}/mods-available/ioncube.ini /etc/php/${PHP_VERSION}/cli/conf.d/00-ioncube.ini || handle_error "Failed to enable ionCube for CLI" ln -sf /etc/php/${PHP_VERSION}/mods-available/ioncube.ini /etc/php/${PHP_VERSION}/fpm/conf.d/00-ioncube.ini || handle_error "Failed to enable ionCube for FPM" + +# Restart PHP-FPM to load ionCube systemctl restart php${PHP_VERSION}-fpm || handle_error "Failed to restart PHP-FPM" # Verify ionCube installation @@ -370,6 +536,8 @@ fi # Start and enable MariaDB log_step "Configuring MariaDB" + +# Initialize MariaDB system database if not already done if [ ! -d "/var/lib/mysql/mysql" ]; then log_info "Initializing MariaDB system database" mysql_install_db --user=mysql --datadir=/var/lib/mysql || handle_error "Failed to initialize MariaDB" @@ -383,60 +551,101 @@ if [ ! -f "/etc/mysql/debian-start" ]; then log_info "Creating missing /etc/mysql/debian-start script" cat > /etc/mysql/debian-start << 'EOF' #!/bin/bash -if [ "$(id -u)" != "0" ]; then echo "This script must be run as root" 1>&2; exit 1; fi -if ! /usr/bin/mysqladmin --defaults-file=/etc/mysql/debian.cnf ping > /dev/null 2>&1; then exit 0; fi +# This script is executed by "/etc/init.d/mysql" on every (re)start. + +# Exit if the script is not being run by root +if [ "$(id -u)" != "0" ]; then + echo "This script must be run as root" 1>&2 + exit 1 +fi + +# Exit successfully if mysql is not running +if ! /usr/bin/mysqladmin --defaults-file=/etc/mysql/debian.cnf ping > /dev/null 2>&1; then + exit 0 +fi + +# Exit successfully exit 0 EOF chmod +x /etc/mysql/debian-start || handle_error "Failed to make debian-start executable" log_success "Created /etc/mysql/debian-start script" fi + COMPLETED_STEPS+=("MariaDB initialized, started and enabled") -# Configure MySQL to allow remote connections -log_step "Configuring MySQL for remote connections" -mkdir -p /etc/mysql/mariadb.conf.d/ || handle_error "Failed to create MariaDB conf directory" +# Configure MySQL to allow remote connections and optimize performance +log_step "Configuring MySQL for remote connections and performance" +# Create MariaDB configuration directory if it doesn't exist +mkdir -p /etc/mysql/mariadb.conf.d/ + +# Configure MariaDB cat > /etc/mysql/mariadb.conf.d/50-server.cnf << 'EOL' -[server] -[mariadbd] +[mysqld] user = mysql pid-file = /run/mysqld/mysqld.pid +socket = /run/mysqld/mysqld.sock +port = 3306 basedir = /usr datadir = /var/lib/mysql tmpdir = /tmp +lc-messages-dir = /usr/share/mysql +lc-messages = en_US skip-external-locking + bind-address = 0.0.0.0 + key_buffer_size = 16M max_allowed_packet = 16M thread_stack = 192K thread_cache_size = 8 + myisam-recover-options = BACKUP + query_cache_limit = 1M query_cache_size = 16M + expire_logs_days = 10 max_binlog_size = 100M + character-set-server = utf8mb4 collation-server = utf8mb4_general_ci + +# Performance optimizations innodb_buffer_pool_size = 1G innodb_log_file_size = 256M innodb_flush_method = O_DIRECT innodb_flush_log_at_trx_commit = 2 innodb_file_per_table = 1 + [embedded] + [mariadb] + +[mariadb-10.6] EOL +# Restart MariaDB to apply changes systemctl restart mariadb || handle_error "Failed to restart MariaDB after configuration change" COMPLETED_STEPS+=("MySQL configured for remote connections") +# Get server hostname and set email +DOMAIN=$(hostname -f) +EMAIL_ADDRESS="simpluxsolutions@gmail.com" + # Generate random credentials or reuse existing ones DB_CREDENTIALS_FILE="/root/db.txt" + +# Check if db.txt exists and contains valid credentials if [ -f "$DB_CREDENTIALS_FILE" ] && [ -r "$DB_CREDENTIALS_FILE" ]; then log_step "Found existing database credentials, reusing them" + + # Extract credentials from existing db.txt file MYSQL_USER=$(grep "^DB_USERNAME=" "$DB_CREDENTIALS_FILE" | cut -d'=' -f2) MYSQL_PASSWORD=$(grep "^DB_PASSWORD=" "$DB_CREDENTIALS_FILE" | cut -d'=' -f2) MYSQL_DATABASE=$(grep "^DB_DATABASE=" "$DB_CREDENTIALS_FILE" | cut -d'=' -f2) + # Validate that we got all required credentials if [ -n "$MYSQL_USER" ] && [ -n "$MYSQL_PASSWORD" ] && [ -n "$MYSQL_DATABASE" ]; then log_info "Reusing existing database credentials: User=$MYSQL_USER, Database=$MYSQL_DATABASE" COMPLETED_STEPS+=("Database credentials reused from existing file") @@ -455,16 +664,6 @@ else COMPLETED_STEPS+=("New database credentials generated") fi -# Save database credentials -log_step "Saving database credentials" -echo "MySQL Credentials:" > "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" -echo "DB_HOST=localhost" >> "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" -echo "DB_PORT=3306" >> "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" -echo "DB_DATABASE=$MYSQL_DATABASE" >> "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" -echo "DB_USERNAME=$MYSQL_USER" >> "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" -echo "DB_PASSWORD=$MYSQL_PASSWORD" >> "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" -COMPLETED_STEPS+=("Database credentials saved") - # Secure MariaDB installation log_step "Securing MariaDB installation" mysql -e "DELETE FROM mysql.user WHERE User='';" || handle_error "Failed to delete anonymous MariaDB user" @@ -477,8 +676,13 @@ COMPLETED_STEPS+=("MariaDB installation secured") # Create database and user log_step "Creating database and user" mysql -e "CREATE DATABASE $MYSQL_DATABASE;" || handle_error "Failed to create database" + +# Create user with access from any host mysql -e "CREATE USER '$MYSQL_USER'@'%' IDENTIFIED BY '$MYSQL_PASSWORD';" || handle_error "Failed to create database user" + +# Grant privileges for all hosts mysql -e "GRANT ALL PRIVILEGES ON $MYSQL_DATABASE.* TO '$MYSQL_USER'@'%';" || handle_error "Failed to grant database privileges" + mysql -e "FLUSH PRIVILEGES;" || handle_error "Failed to flush MariaDB privileges" COMPLETED_STEPS+=("Database and user created with full access") @@ -493,73 +697,25 @@ if ! command -v composer &> /dev/null; then fi COMPLETED_STEPS+=("Composer installed") -# Configure Nginx -log_step "Configuring Nginx" -mv /etc/nginx/sites-available/default /etc/nginx/sites-available/default.bak || handle_error "Failed to backup default Nginx site" -touch /etc/nginx/sites-available/default || handle_error "Failed to create new Nginx site" -ln -sf /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default || handle_error "Failed to symlink Nginx site" - -cat > /etc/nginx/sites-available/default << EOL -server { - listen 80; - listen [::]:80; - - root /var/www/html/public; - index index.php index.html index.htm index.nginx-debian.html; - - server_name $DOMAIN; - - location / { - try_files \$uri \$uri/ /index.php?\$query_string; - } - - location ~ \.php$ { - include snippets/fastcgi-php.conf; - fastcgi_pass unix:/var/run/php/php${PHP_VERSION}-fpm.sock; - } - - location ~ /\.ht { - deny all; - } -} -EOL -COMPLETED_STEPS+=("Nginx configured") - -# Configure SSL with Certbot -log_step "Configuring SSL with Certbot" -echo "Configuring SSL certificate for $DOMAIN" - -if [ -d "/etc/letsencrypt/live/$DOMAIN" ]; then - log_info "SSL certificate already exists for $DOMAIN, reusing existing certificate" - echo -e "1\n" | certbot --nginx -d "$DOMAIN" || handle_error "Failed to reuse SSL certificate" - COMPLETED_STEPS+=("SSL certificate reused (already exists)") -else - log_info "No existing SSL certificate found for $DOMAIN, requesting new certificate" - certbot --nginx -d "$DOMAIN" --agree-tos --email "$EMAIL_ADDRESS" --no-eff-email --non-interactive --redirect || handle_error "Failed to configure SSL with Certbot" - COMPLETED_STEPS+=("SSL configured with Certbot") -fi - -# Test the Nginx configuration (Graceful restart applied) -log_step "Restarting Nginx" -nginx -t || handle_error "Nginx configuration failed" -systemctl restart nginx || handle_error "Failed to restart Nginx" -COMPLETED_STEPS+=("Nginx restarted gracefully") # Setup Laravel application log_step "Setting up Laravel application" LOCAL_PATH="/var/www/html" REPO_URL="$GITHUB_REPO_URL" + +# Remove existing web root if it exists (no backup) if [ -d "$LOCAL_PATH" ]; then rm -rf "$LOCAL_PATH" || handle_error "Failed to remove existing web root" fi -git clone -b master "$REPO_URL" "$LOCAL_PATH" || handle_error "Failed to clone repository" +# Clone the repository +git clone "$REPO_URL" "$LOCAL_PATH" || handle_error "Failed to clone repository" cd "$LOCAL_PATH" || handle_error "Failed to change directory to web root" # Install Laravel dependencies log_step "Installing Laravel dependencies" -composer install --no-interaction || handle_error "Failed to install Laravel dependencies" +COMPOSER_ALLOW_SUPERUSER=1 composer install --no-interaction --no-security-blocking --prefer-dist || handle_error "Failed to install Laravel dependencies" COMPLETED_STEPS+=("Laravel dependencies installed") # Create and configure .env file @@ -578,45 +734,83 @@ sed -i "s|APP_URL=.*|APP_URL=https://$DOMAIN|" .env || handle_error "Failed to u sed -i "s|APP_NAME=.*|APP_NAME=\"$DOMAIN\"|" .env || handle_error "Failed to update APP_NAME in .env" COMPLETED_STEPS+=(".env file updated with database credentials") + +# Restart services +log_step "Restarting services" +systemctl restart mariadb || handle_error "Failed to restart MariaDB" +systemctl restart php${PHP_VERSION}-fpm || handle_error "Failed to restart PHP ${PHP_VERSION} FPM" +systemctl restart supervisor || handle_error "Failed to restart Supervisor" +systemctl restart openvpn || handle_error "Failed to restart OpenVPN" +COMPLETED_STEPS+=("Services restarted") + # Run Laravel migrations and seed the database log_step "Running Laravel migrations and seeding database" php artisan migrate --force || handle_error "Failed to run Laravel migrations" php artisan db:seed --force || handle_error "Failed to seed database" COMPLETED_STEPS+=("Laravel migrations run and database seeded") -# Set correct www permissions -log_step "Setting correct www permissions" -chown -R www-data:www-data /var/www/html || handle_error "Failed to set ownership of web root" -chmod -R 775 /var/www/html/storage || handle_error "Failed to set permissions of storage directory" -chmod -R 775 /var/www/html/bootstrap/cache || handle_error "Failed to set permissions of cache directory" - -# Make the scripts executable -chmod +x /var/www/html/sh/set_permissions.sh || handle_error "Failed to make permissions script executable" -chmod +x /var/www/html/sh/restart-services.sh || handle_error "Failed to make services script executable" - -# Run the script once to apply initial configuration -/var/www/html/sh/set_permissions.sh || handle_error "Failed to run permissions script" -/var/www/html/sh/restart-services.sh || handle_error "Failed to run services script" -COMPLETED_STEPS+=("Correct www permissions set") - # Optimize RADIUS database indexes log_step "Optimizing RADIUS database indexes" cat > /tmp/radius_optimize.sql << "EOL" USE radius; -ALTER TABLE radcheck ADD INDEX idx_username (username), ADD INDEX idx_attribute (attribute); + +-- Add indexes to improve query performance + +-- radcheck +ALTER TABLE radcheck + ADD INDEX idx_username (username), + ADD INDEX idx_attribute (attribute); ANALYZE TABLE radcheck; -ALTER TABLE radreply ADD INDEX idx_username (username), ADD INDEX idx_attribute (attribute); + +-- radreply +ALTER TABLE radreply + ADD INDEX idx_username (username), + ADD INDEX idx_attribute (attribute); ANALYZE TABLE radreply; -ALTER TABLE radusergroup ADD INDEX idx_username (username), ADD INDEX idx_groupname (groupname); + +-- radusergroup +ALTER TABLE radusergroup + ADD INDEX idx_username (username), + ADD INDEX idx_groupname (groupname); ANALYZE TABLE radusergroup; -ALTER TABLE radgroupcheck ADD INDEX idx_groupname (groupname), ADD INDEX idx_attribute (attribute); + +-- radgroupcheck +ALTER TABLE radgroupcheck + ADD INDEX idx_groupname (groupname), + ADD INDEX idx_attribute (attribute); ANALYZE TABLE radgroupcheck; -ALTER TABLE radgroupreply ADD INDEX idx_groupname (groupname), ADD INDEX idx_attribute (attribute); + +-- radgroupreply +ALTER TABLE radgroupreply + ADD INDEX idx_groupname (groupname), + ADD INDEX idx_attribute (attribute); ANALYZE TABLE radgroupreply; -ALTER TABLE radacct ADD INDEX idx_username (username), ADD INDEX idx_acctsessionid (acctsessionid), ADD INDEX idx_framedipaddress (framedipaddress), ADD INDEX idx_acctstarttime (acctstarttime), ADD INDEX idx_acctstoptime (acctstoptime), ADD INDEX idx_nasipaddress (nasipaddress), ADD INDEX idx_calledstationid (calledstationid), ADD INDEX idx_callingstationid (callingstationid); + +-- radacct (very critical for performance) +ALTER TABLE radacct + ADD INDEX idx_username (username), + ADD INDEX idx_acctsessionid (acctsessionid), + ADD INDEX idx_framedipaddress (framedipaddress), + ADD INDEX idx_acctstarttime (acctstarttime), + ADD INDEX idx_acctstoptime (acctstoptime), + ADD INDEX idx_nasipaddress (nasipaddress), + ADD INDEX idx_calledstationid (calledstationid), + ADD INDEX idx_callingstationid (callingstationid); ANALYZE TABLE radacct; -ALTER TABLE radpostauth ADD INDEX idx_username (username), ADD INDEX idx_reply (reply), ADD INDEX idx_authdate (authdate); + +-- radpostauth +ALTER TABLE radpostauth + ADD INDEX idx_username (username), + ADD INDEX idx_reply (reply), + ADD INDEX idx_authdate (authdate); ANALYZE TABLE radpostauth; + +-- hotspot_sessions +ALTER TABLE hotspot_sessions + ADD INDEX idx_payment_voucher (payment_id, voucher); +ANALYZE TABLE hotspot_sessions; + +-- Convert tables to InnoDB and utf8mb4 (recommended for reliability and Unicode support) ALTER TABLE radcheck ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE radreply ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE radusergroup ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; @@ -624,6 +818,9 @@ ALTER TABLE radgroupcheck ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLAT ALTER TABLE radgroupreply ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE radacct ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE radpostauth ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE hotspot_sessions ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- Analyze again after engine/charset conversion ANALYZE TABLE radcheck; ANALYZE TABLE radreply; ANALYZE TABLE radusergroup; @@ -631,128 +828,66 @@ ANALYZE TABLE radgroupcheck; ANALYZE TABLE radgroupreply; ANALYZE TABLE radacct; ANALYZE TABLE radpostauth; +ANALYZE TABLE hotspot_sessions; + EOL mysql -u root < /tmp/radius_optimize.sql || handle_error "Failed to optimize RADIUS database indexes" -rm -f /tmp/radius_optimize.sql || handle_error "Failed to remove radius optimization sql file" +rm -f /tmp/radius_optimize.sql COMPLETED_STEPS+=("RADIUS database indexes optimized") -# Configure Supervisor for queue worker (Pre-created log dir fix) -log_step "Configuring Supervisor for queue worker" -mkdir -p /var/www/html/storage/logs || handle_error "Failed to create Laravel logs directory" -cat > /etc/supervisor/conf.d/queue-worker.conf << "EOL" -[program:queue-worker] -process_name=%(program_name)s_%(process_num)02d -command=php /var/www/html/artisan queue:work --tries=3 -autostart=true -autorestart=true -stopasgroup=true -killasgroup=true -user=www-data -numprocs=5 -redirect_stderr=true -stdout_logfile=/var/www/html/storage/logs/queue-worker.log -EOL -COMPLETED_STEPS+=("Supervisor configured for queue worker") +# Configure FreeRADIUS +log_step "Configuring FreeRADIUS" -# Install OpenVPN based on Ubuntu version -log_step "Installing OpenVPN" -case $UBUNTU_VERSION in - "focal"|"jammy"|"noble") - echo "Installing OpenVPN for Ubuntu $UBUNTU_VERSION" - export AUTO_INSTALL=y - curl -O https://raw.githubusercontent.com/simpleisp/bash/main/openvpn.sh || handle_error "Failed to download OpenVPN installer" - chmod +x openvpn.sh || handle_error "Failed to make OpenVPN installer executable" - ./openvpn.sh || handle_error "Failed to install OpenVPN" - - # Set more secure permissions for OpenVPN - chown -R root:root /etc/openvpn || handle_error "Failed to set ownership of OpenVPN configuration directory" - chmod -R 777 /etc/openvpn || handle_error "Failed to set permissions of OpenVPN configuration directory" - chmod -R 777 /etc/openvpn/easy-rsa || handle_error "Failed to set permissions of OpenVPN easy-rsa directory" - ;; - *) - handle_error "Unsupported Ubuntu version for OpenVPN installation" - ;; -esac -COMPLETED_STEPS+=("OpenVPN installed") - -# Configure Systemd sandbox overrides for OpenVPN writes -log_step "Configuring Systemd sandbox overrides for OpenVPN writes" -mkdir -p /etc/systemd/system/php${PHP_VERSION}-fpm.service.d || handle_error "Failed to create PHP systemd override directory" -cat > /etc/systemd/system/php${PHP_VERSION}-fpm.service.d/override.conf << 'EOF' -[Service] -ReadWritePaths=/etc/openvpn -EOF +# Enable buffered-sql site +log_step "Enabling buffered-sql site" +# Ensure the sites-enabled directory exists +mkdir -p /etc/freeradius/sites-enabled || handle_error "Failed to create FreeRADIUS sites-enabled directory" +ln -sf /etc/freeradius/sites-available/buffered-sql /etc/freeradius/sites-enabled/buffered-sql || handle_error "Failed to enable buffered-sql site" +COMPLETED_STEPS+=("FreeRADIUS buffered-sql site enabled") -mkdir -p /etc/systemd/system/supervisor.service.d || handle_error "Failed to create Supervisor systemd override directory" -cat > /etc/systemd/system/supervisor.service.d/override.conf << 'EOF' -[Service] -ReadWritePaths=/etc/openvpn -EOF -COMPLETED_STEPS+=("Systemd sandbox overrides configured") +# Enable SQL module for FreeRADIUS +log_step "Enabling SQL module" +# Ensure the mods-enabled directory exists +mkdir -p /etc/freeradius/mods-enabled || handle_error "Failed to create FreeRADIUS mods-enabled directory" +ln -sf /etc/freeradius/mods-available/sql /etc/freeradius/mods-enabled/sql || handle_error "Failed to enable SQL module" +COMPLETED_STEPS+=("FreeRADIUS SQL module enabled") -# Install Laravel cron (Safe Append Fix) -log_step "Installing cron" -(crontab -l 2>/dev/null; echo "* * * * * php /var/www/html/artisan schedule:run >> /dev/null 2>&1") | crontab - || handle_error "Failed to install Laravel cron job" -COMPLETED_STEPS+=("Cron job installed") +# Configure FreeRADIUS REST module +log_step "Configuring FreeRADIUS REST module" +REST_CONFIG="/etc/freeradius/mods-available/rest" -# Update sudoers for www-data user (Updated openvpn explicit target) -log_step "Updating sudoers for www-data user" -cat >> /etc/sudoers << 'EOL' -www-data ALL=NOPASSWD: /bin/systemctl start openvpn@server -www-data ALL=NOPASSWD: /bin/systemctl stop openvpn@server -www-data ALL=NOPASSWD: /bin/systemctl restart openvpn@server -www-data ALL=NOPASSWD: /bin/systemctl status openvpn@server -www-data ALL=NOPASSWD: /bin/systemctl reload openvpn@server -www-data ALL=NOPASSWD: /bin/systemctl enable openvpn@server -www-data ALL=NOPASSWD: /bin/systemctl disable openvpn@server -www-data ALL=NOPASSWD: /bin/systemctl start freeradius -www-data ALL=NOPASSWD: /bin/systemctl stop freeradius -www-data ALL=NOPASSWD: /bin/systemctl restart freeradius -www-data ALL=NOPASSWD: /bin/systemctl status freeradius -www-data ALL=NOPASSWD: /bin/systemctl reload freeradius -www-data ALL=NOPASSWD: /bin/systemctl enable freeradius -www-data ALL=NOPASSWD: /bin/systemctl disable freeradius -www-data ALL=NOPASSWD: /bin/supervisorctl stop all -www-data ALL=NOPASSWD: /bin/supervisorctl reread -www-data ALL=NOPASSWD: /bin/supervisorctl update -www-data ALL=NOPASSWD: /bin/supervisorctl start all -www-data ALL=NOPASSWD: /bin/supervisorctl restart all -www-data ALL=NOPASSWD: /bin/supervisorctl status -www-data ALL=NOPASSWD: /bin/systemctl restart supervisor -www-data ALL=NOPASSWD: /bin/systemctl status ssh -www-data ALL=NOPASSWD: /var/www/html/sh/set_permissions.sh -www-data ALL=NOPASSWD: /var/www/html/sh/restart-services.sh -EOL -COMPLETED_STEPS+=("Sudoers updated for www-data user") +# REST module disabled for SimpleISP to avoid connection errors during configuration test +#rm /etc/freeradius/mods-enabled/rest || handle_error "Failed to disable REST module" -# Open Firewall Ports and enable ufw -log_step "Opening firewall ports and enabling ufw" -ufw allow ssh || handle_error "Failed to allow SSH through firewall" -ufw allow 9080/tcp || handle_error "Failed to allow port 9080 through firewall" -ufw allow http || handle_error "Failed to allow HTTP through firewall" -ufw allow https || handle_error "Failed to allow HTTPS through firewall" -ufw allow 1194/tcp || handle_error "Failed to allow OpenVPN through firewall" -ufw allow 1812:1813/udp || handle_error "Failed to allow FreeRADIUS through firewall" -ufw reload || handle_error "Failed to reload firewall rules" -yes | ufw enable || handle_error "Failed to enable firewall" -COMPLETED_STEPS+=("Firewall ports opened and ufw enabled") +if [ -f "$REST_CONFIG" ]; then + # Update connect_uri to use domain/api instead of localhost + sed -i 's|connect_uri = "http://127.0.0.1/"|connect_uri = "https://'$DOMAIN'/api"|g' "$REST_CONFIG" || handle_error "Failed to configure REST module connect_uri" + # Also handle the commented version + sed -i 's|# connect_uri = "http://127.0.0.1/"|connect_uri = "https://'$DOMAIN'/api"|g' "$REST_CONFIG" || true +fi -# Test FreeRADIUS configuration -log_step "Checking FreeRADIUS files" +COMPLETED_STEPS+=("FreeRADIUS REST module configured (disabled for SimpleISP)") +# Ensure FreeRADIUS configuration files exist (restore if missing) +log_step "Checking FreeRADIUS radiusd.conf" if [ ! -f "/etc/freeradius/radiusd.conf" ]; then log_info "FreeRADIUS configuration files missing, reinstalling and reconfiguring FreeRADIUS package" + + # Purge and reinstall FreeRADIUS to ensure clean configuration apt-get purge -y freeradius freeradius-common freeradius-config 2>/dev/null || echo "FreeRADIUS not installed" apt-get autoremove -y 2>/dev/null - apt-get install -y freeradius freeradius-mysql freeradius-config || handle_error "Failed to reinstall FreeRADIUS" + apt-get install -y freeradius freeradius-mysql freeradius-utils || handle_error "Failed to reinstall FreeRADIUS" + + # Reconfigure the package to ensure configuration files are created dpkg-reconfigure -f noninteractive freeradius-config 2>/dev/null || echo "Reconfigure not needed" -fi - -if [ ! -f "/etc/freeradius/radiusd.conf" ]; then - log_info "Creating minimal radiusd.conf configuration" - mkdir -p /etc/freeradius || handle_error "Failed to create freeradius config dir" - cat > /etc/freeradius/radiusd.conf << 'EOF' + + # Verify configuration file was created + if [ ! -f "/etc/freeradius/radiusd.conf" ]; then + # Create a minimal radiusd.conf if still missing + log_info "Creating minimal radiusd.conf configuration" + mkdir -p /etc/freeradius + cat > /etc/freeradius/radiusd.conf << 'EOF' prefix = /usr exec_prefix = ${prefix} sysconfdir = /etc @@ -770,83 +905,91 @@ cadir = ${confdir}/certs run_dir = ${localstatedir}/run/${name} db_dir = ${raddbdir} + libdir = /usr/lib/freeradius + pidfile = ${run_dir}/${name}.pid correct_escapes = true + max_request_time = 30 cleanup_delay = 5 max_requests = 16384 + hostname_lookups = no log { -destination = files -colourise = yes -file = ${logdir}/radius.log -syslog_facility = daemon -stripped_names = no -auth = no -auth_badpass = no -auth_goodpass = no -msg_denied = "You are already logged in - access denied" + destination = files + colourise = yes + file = ${logdir}/radius.log + syslog_facility = daemon + stripped_names = no + auth = no + auth_badpass = no + auth_goodpass = no + msg_denied = "You are already logged in - access denied" } checkrad = ${sbindir}/checkrad security { -allow_core_dumps = no -max_attributes = 200 -reject_delay = 1 -status_server = yes + allow_core_dumps = no + max_attributes = 200 + reject_delay = 1 + status_server = yes } proxy_requests = yes $INCLUDE proxy.conf + $INCLUDE clients.conf thread pool { -start_servers = 5 -max_servers = 32 -min_spare_servers = 3 -max_spare_servers = 10 -max_requests_per_server = 0 -auto_limit_acct = no + start_servers = 5 + max_servers = 32 + min_spare_servers = 3 + max_spare_servers = 10 + max_requests_per_server = 0 + auto_limit_acct = no } $INCLUDE sites-enabled/ + $INCLUDE mods-enabled/ policy { -$INCLUDE policy.d/ + $INCLUDE policy.d/ } instantiate { } EOF - chmod 644 /etc/freeradius/radiusd.conf || handle_error "Failed to set radiusd.conf permissions" - log_success "Created minimal radiusd.conf configuration" -fi - -if [ -f "/etc/freeradius/mods-available/sql" ]; then - ln -sf /etc/freeradius/mods-available/sql /etc/freeradius/mods-enabled/ || handle_error "Failed to re-enable SQL module" + chmod 644 /etc/freeradius/radiusd.conf + log_success "Created minimal radiusd.conf configuration" + fi + + # Re-enable modules after reinstallation + if [ -f "/etc/freeradius/mods-available/sql" ]; then + ln -sf /etc/freeradius/mods-available/sql /etc/freeradius/mods-enabled/ || handle_error "Failed to re-enable SQL module" + fi + if [ -f "/etc/freeradius/mods-available/rest" ]; then + # REST module disabled for SimpleISP to avoid connection errors during configuration test + # ln -sf /etc/freeradius/mods-available/rest /etc/freeradius/mods-enabled/rest || handle_error "Failed to re-enable REST module" + log_info "REST module configuration skipped for SimpleISP" + fi + log_success "FreeRADIUS configuration files restored" fi -COMPLETED_STEPS+=("Completed checking FreeRADIUS files") - -# Enable buffered-sql site -log_step "Enabling FreeRADIUS buffered-sql site" -mkdir -p /etc/freeradius/sites-enabled || handle_error "Failed to create FreeRADIUS sites-enabled directory" -ln -sf /etc/freeradius/sites-available/buffered-sql /etc/freeradius/sites-enabled/buffered-sql || handle_error "Failed to enable buffered-sql site" -COMPLETED_STEPS+=("FreeRADIUS buffered-sql site enabled") -# Enable SQL module for FreeRADIUS -log_step "Enabling SQL module" -mkdir -p /etc/freeradius/mods-enabled || handle_error "Failed to create FreeRADIUS mods-enabled directory" -ln -sf /etc/freeradius/mods-available/sql /etc/freeradius/mods-enabled/sql || handle_error "Failed to enable SQL module" -COMPLETED_STEPS+=("FreeRADIUS SQL module enabled") +COMPLETED_STEPS+=("Completed checking radiusd.conf") # Write new FreeRADIUS SQL module log_step "Writing new FreeRADIUS SQL module" + SQL_FILE="/etc/freeradius/mods-available/sql" + +# Backup if it exists +[ -f "$SQL_FILE" ] && cp "$SQL_FILE" "${SQL_FILE}.bak.$(date +%Y%m%d-%H%M%S)" + cat > "$SQL_FILE" < /tmp/new_accounting_block -accounting { -sql -exec -attr_filter.accounting_response -} -EOF - awk ' - BEGIN { skip = 0 } - /^accounting[ \t]*{/ { print_block = 1; print_file("/tmp/new_accounting_block"); skip = 1; next } - /^[ \t]*}/ { if (skip) { skip = 0; next } } - !skip { print } - function print_file(file) { - while ((getline line < file) > 0) print line; - close(file) - } - ' "$DEFAULT_SITE" > /tmp/tmp_site && mv /tmp/tmp_site "$DEFAULT_SITE" -else - handle_error "Default site configuration file not found" + # Change -sql to sql + sed -i 's/-sql/sql/g' "$DEFAULT_SITE" || handle_error "Failed to update -sql to sql in FreeRADIUS default site configuration" + + # Comment out detail line + sed -i 's/^[[:space:]]*detail/# detail/' "$DEFAULT_SITE" || handle_error "Failed to comment out detail line in FreeRADIUS default site configuration" fi + COMPLETED_STEPS+=("FreeRADIUS default site configured") -# Apply Systemd Sandbox changes from the overrides -systemctl daemon-reload || handle_error "Failed to reload systemd daemon" +# Start and enable FreeRADIUS now that database tables are ready +log_step "Starting and enabling FreeRADIUS" +systemctl start freeradius || handle_error "Failed to start FreeRADIUS" +systemctl enable freeradius || handle_error "Failed to enable FreeRADIUS" +systemctl restart freeradius || handle_error "Failed to restart FreeRADIUS" +COMPLETED_STEPS+=("FreeRADIUS started and enabled") -# Enable and start all services (Consolidated Block) -log_step "Enabling and restarting all services" +# Configure Supervisor for queue worker +log_step "Configuring Supervisor for queue worker" +cat > /etc/supervisor/conf.d/queue-worker.conf << "EOL" +[program:queue-worker] +process_name=%(program_name)s_%(process_num)02d +command=php /var/www/html/artisan queue:work --tries=3 +autostart=true +autorestart=true +stopasgroup=true +killasgroup=true +user=www-data +numprocs=5 +redirect_stderr=true +stdout_logfile=/var/www/html/storage/logs/queue-worker.log +EOL +COMPLETED_STEPS+=("Supervisor configured for queue worker") -SERVICES=(nginx php${PHP_VERSION}-fpm supervisor openvpn@server freeradius valkey-server) -for service in "${SERVICES[@]}"; do - systemctl enable "$service" || handle_error "Failed to enable $service" - systemctl restart "$service" || handle_error "Failed to restart $service" -done -COMPLETED_STEPS+=("All services enabled and restarted") +# Install OpenVPN based on Ubuntu version +log_step "Installing OpenVPN" +case $UBUNTU_VERSION in + "focal"|"jammy"|"noble") + echo "Installing OpenVPN for Ubuntu $UBUNTU_VERSION" + export AUTO_INSTALL=y + curl -O https://raw.githubusercontent.com/simpleisp/bash/main/openvpn.sh || handle_error "Failed to download OpenVPN installer" + chmod +x openvpn.sh || handle_error "Failed to make OpenVPN installer executable" + ./openvpn.sh || handle_error "Failed to install OpenVPN" + + # Enable and start OpenVPN service + systemctl enable openvpn || handle_error "Failed to enable OpenVPN service" + systemctl start openvpn || handle_error "Failed to start OpenVPN service" + + # Set more secure permissions for OpenVPN + chown -R root:root /etc/openvpn || handle_error "Failed to set ownership of OpenVPN configuration directory" + chmod -R 777 /etc/openvpn || handle_error "Failed to set permissions of OpenVPN configuration directory" + chmod -R 777 /etc/openvpn/easy-rsa || handle_error "Failed to set permissions of OpenVPN easy-rsa directory" + ;; + *) + handle_error "Unsupported Ubuntu version for OpenVPN installation: $UBUNTU_VERSION" + ;; +esac +COMPLETED_STEPS+=("OpenVPN installed") + +# Configure Nginx +log_step "Configuring Nginx" +cat > /etc/nginx/sites-available/default << EOL +server { -# Create a script to update memory configurations -cat > /usr/local/bin/update_memory_config.sh << 'EOL' -#!/usr/bin/env bash -# Optimized Co-hosted Tuning Script for SimpleISP -# Balances MariaDB, Valkey, PHP-FPM, and FreeRADIUS. + listen 80; + listen [::]:80; + + root /var/www/html/public; + index index.php index.html index.htm index.nginx-debian.html; -set -euo pipefail + server_name $DOMAIN; -LOG_FILE="/var/log/memory_config_updates.log" -log() { echo "[$(date +'%F %T')] $1" >> "$LOG_FILE"; } + location / { + try_files \$uri \$uri/ /index.php?\$query_string; + } -log "Starting memory configuration update" + location ~ \.php$ { + include snippets/fastcgi-php.conf; + fastcgi_pass unix:/var/run/php/php${PHP_VERSION}-fpm.sock; + } -# 1. Introspect System Resources -TOTAL_RAM_MB=$(free -m | awk '/^Mem:/{print $2}') -VCPUS=$(nproc --all) + location ~ /\.ht { + deny all; + } -log "Detected RAM: ${TOTAL_RAM_MB}MB | CPU Cores: ${VCPUS}" +} +EOL +COMPLETED_STEPS+=("Nginx configured") -# 2. Budget Memory -# OS Reserve: 15% or 1024MB, whichever is higher -OS_RESERVE_MB=$(( TOTAL_RAM_MB * 15 / 100 )) -[ "$OS_RESERVE_MB" -lt 1024 ] && OS_RESERVE_MB=1024 +# Enable the default site +log_step "Enabling Nginx default site" +ln -sf /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default || handle_error "Failed to enable Nginx default site" +COMPLETED_STEPS+=("Nginx default site enabled") -# MariaDB: 40% of total RAM -MARIADB_POOL_MB=$(( TOTAL_RAM_MB * 40 / 100 )) -[ "$MARIADB_POOL_MB" -lt 256 ] && MARIADB_POOL_MB=256 +# Test the Nginx configuration +log_step "Testing Nginx configuration" +nginx -t || handle_error "Failed to test Nginx configuration" +COMPLETED_STEPS+=("Nginx configuration tested") -# Valkey/Redis: 15% of total RAM -VALKEY_MB=$(( TOTAL_RAM_MB * 15 / 100 )) -[ "$VALKEY_MB" -lt 128 ] && VALKEY_MB=128 +# If the configuration is OK, reload and restart Nginx +log_step "Reloading and restarting Nginx" +systemctl reload nginx || handle_error "Failed to reload Nginx" +systemctl restart nginx || handle_error "Failed to restart Nginx" +COMPLETED_STEPS+=("Nginx reloaded and restarted") -# PHP-FPM: The remaining RAM -REMAINING_MB=$(( TOTAL_RAM_MB - OS_RESERVE_MB - MARIADB_POOL_MB - VALKEY_MB )) -[ "$REMAINING_MB" -lt 256 ] && REMAINING_MB=256 +# Set correct permissions +log_step "Setting correct permissions" +chown -R www-data:www-data /var/www/html || handle_error "Failed to set ownership of web root" +chmod -R 775 /var/www/html/storage || handle_error "Failed to set permissions of storage directory" +chmod -R 775 /var/www/html/bootstrap/cache || handle_error "Failed to set permissions of cache directory" -log "Budget -> OS: ${OS_RESERVE_MB}MB | DB: ${MARIADB_POOL_MB}MB | Cache: ${VALKEY_MB}MB | PHP: ${REMAINING_MB}MB" +# Make the scripts executable +chmod +x /var/www/html/sh/set_permissions.sh || handle_error "Failed to make permissions script executable" +chmod +x /var/www/html/sh/restart-services.sh || handle_error "Failed to make services script executable" -# 3. Apply MariaDB Tuning -MARIADB_CONF="/etc/mysql/mariadb.conf.d/99-autotune.cnf" -log "Tuning MariaDB..." -cat > "$MARIADB_CONF" < 1024 ? MARIADB_POOL_MB / 1024 : 1 )) -innodb_log_file_size = $(( MARIADB_POOL_MB / 4 ))M -max_connections = $(( VCPUS * 150 )) -tmp_table_size = 64M -max_heap_table_size = 64M -EOF -systemctl restart mariadb || service mysql restart - -# 4. Apply Valkey Tuning -VALKEY_CONF="/etc/valkey/valkey.conf" -if [ -f "$VALKEY_CONF" ]; then - log "Tuning Valkey..." - # Replace existing maxmemory line or append it - sed -i -E "s/^#?maxmemory .*/maxmemory ${VALKEY_MB}mb/" "$VALKEY_CONF" - # Ensure eviction policy is set safely for Laravel (protects queues/sessions without expirations) - if ! grep -q "^maxmemory-policy" "$VALKEY_CONF"; then - echo "maxmemory-policy volatile-lru" >> "$VALKEY_CONF" - fi - systemctl restart valkey-server || true -fi +# Run the script once to apply initial configuration +/var/www/html/sh/set_permissions.sh || handle_error "Failed to run permissions script" +/var/www/html/sh/restart-services.sh || handle_error "Failed to run services script" -# 5. Apply PHP-FPM Tuning -# Assuming Laravel average process size = 80MB -AVG_PHP_RSS_MB=80 -MAX_CHILDREN=$(( REMAINING_MB / AVG_PHP_RSS_MB )) -[ "$MAX_CHILDREN" -lt 5 ] && MAX_CHILDREN=5 - -# Find active PHP-FPM pool file -POOL_CONF=$(find /etc/php/*/fpm/pool.d/ -name "www.conf" | head -n 1) -if [ -n "$POOL_CONF" ]; then - log "Tuning PHP-FPM in $POOL_CONF..." - sed -i -E "s/^[;#]?pm.max_children =.*/pm.max_children = ${MAX_CHILDREN}/" "$POOL_CONF" - sed -i -E "s/^[;#]?pm.start_servers =.*/pm.start_servers = $(( MAX_CHILDREN * 20 / 100 + 1 ))/" "$POOL_CONF" - sed -i -E "s/^[;#]?pm.min_spare_servers =.*/pm.min_spare_servers = $(( MAX_CHILDREN * 10 / 100 + 1 ))/" "$POOL_CONF" - sed -i -E "s/^[;#]?pm.max_spare_servers =.*/pm.max_spare_servers = $(( MAX_CHILDREN * 30 / 100 + 1 ))/" "$POOL_CONF" - - # Reload PHP-FPM - PHP_SVC=$(basename $(dirname $(dirname "$POOL_CONF")) | sed 's|^|php|;s|$|-fpm|') - systemctl reload "$PHP_SVC" || systemctl restart "$PHP_SVC" -fi +COMPLETED_STEPS+=("Correct permissions set") -# 6. Apply FreeRADIUS Tuning -RADIUS_CONF=$(find /etc/freeradius -name "radiusd.conf" 2>/dev/null | head -n 1) -if [ -n "$RADIUS_CONF" ]; then - log "Tuning FreeRADIUS in $RADIUS_CONF..." - # Start 2 threads per core, max out at 6 per core - START_SERVERS=$(( VCPUS * 2 )) - MAX_SERVERS=$(( VCPUS * 6 )) - - awk -v s="$START_SERVERS" -v m="$MAX_SERVERS" ' - BEGIN { inpool=0 } - /^thread pool \{/ { inpool=1; print; next } - /^\}/ { inpool=0; print; next } - inpool && /^[[:space:]]*start_servers/ { printf "\tstart_servers = %s\n", s; next } - inpool && /^[[:space:]]*max_servers/ { printf "\tmax_servers = %s\n", m; next } - { print } - ' "$RADIUS_CONF" > "${RADIUS_CONF}.tmp" && mv "${RADIUS_CONF}.tmp" "$RADIUS_CONF" - - systemctl restart freeradius || true -fi +# Install cron +log_step "Installing cron" +# Write cron job entry to a temporary file +echo "* * * * * php /var/www/html/artisan schedule:run >> /dev/null 2>&1" > cronjob || handle_error "Failed to write cron job to temporary file" + +# Install the cron job from the temporary file +crontab cronjob || handle_error "Failed to install cron job" +COMPLETED_STEPS+=("Cron job installed") + +# Clean up the temporary file +rm cronjob || handle_error "Failed to remove temporary cron job file" +COMPLETED_STEPS+=("Temporary cron job file removed") -log "Memory configuration update completed" +# Update sudoers for www-data user +log_step "Updating sudoers for www-data user" +cat >> /etc/sudoers << "EOL" +www-data ALL=NOPASSWD: /bin/systemctl start openvpn +www-data ALL=NOPASSWD: /bin/systemctl stop openvpn +www-data ALL=NOPASSWD: /bin/systemctl restart openvpn +www-data ALL=NOPASSWD: /bin/systemctl status openvpn +www-data ALL=NOPASSWD: /bin/systemctl reload openvpn +www-data ALL=NOPASSWD: /bin/systemctl enable openvpn +www-data ALL=NOPASSWD: /bin/systemctl disable openvpn +www-data ALL=NOPASSWD: /bin/systemctl start freeradius +www-data ALL=NOPASSWD: /bin/systemctl stop freeradius +www-data ALL=NOPASSWD: /bin/systemctl restart freeradius +www-data ALL=NOPASSWD: /bin/systemctl status freeradius +www-data ALL=NOPASSWD: /bin/systemctl reload freeradius +www-data ALL=NOPASSWD: /bin/systemctl enable freeradius +www-data ALL=NOPASSWD: /bin/systemctl disable freeradius +www-data ALL=NOPASSWD: /bin/supervisorctl stop all +www-data ALL=NOPASSWD: /bin/supervisorctl reread +www-data ALL=NOPASSWD: /bin/supervisorctl update +www-data ALL=NOPASSWD: /bin/supervisorctl start all +www-data ALL=NOPASSWD: /bin/supervisorctl restart all +www-data ALL=NOPASSWD: /bin/supervisorctl status +www-data ALL=NOPASSWD: /bin/systemctl restart supervisor +www-data ALL=NOPASSWD: /bin/systemctl status ssh +www-data ALL=NOPASSWD: /var/www/html/sh/set_permissions.sh +www-data ALL=NOPASSWD: /var/www/html/sh/restart-services.sh EOL +COMPLETED_STEPS+=("Sudoers updated for www-data user") -# Make the script executable -chmod +x /usr/local/bin/update_memory_config.sh || handle_error "Failed to make memory config script executable" +# Save database credentials +log_step "Saving database credentials" +echo "MySQL Credentials:" > "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" +echo "DB_HOST=localhost" >> "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" +echo "DB_PORT=3306" >> "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" +echo "DB_DATABASE=$MYSQL_DATABASE" >> "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" +echo "DB_USERNAME=$MYSQL_USER" >> "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" +echo "DB_PASSWORD=$MYSQL_PASSWORD" >> "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" +COMPLETED_STEPS+=("Database credentials saved") -# Add cron job to run at 3 AM daily (Safe Append Fix applied) -(crontab -l 2>/dev/null; echo "0 3 * * * /usr/local/bin/update_memory_config.sh") | crontab - || handle_error "Failed to add cron job" +# Start and enable all services +log_step "Starting and enabling all services" +systemctl start nginx || handle_error "Failed to start Nginx" +systemctl enable nginx || handle_error "Failed to enable Nginx" +systemctl start php${PHP_VERSION}-fpm || handle_error "Failed to start PHP ${PHP_VERSION} FPM" +systemctl enable php${PHP_VERSION}-fpm || handle_error "Failed to enable PHP ${PHP_VERSION} FPM" +systemctl start supervisor || handle_error "Failed to start Supervisor" +systemctl enable supervisor || handle_error "Failed to enable Supervisor" +systemctl start openvpn || handle_error "Failed to start OpenVPN" +systemctl enable openvpn || handle_error "Failed to enable OpenVPN" +COMPLETED_STEPS+=("Core services started and enabled") + +# Restart all services to ensure proper configuration +log_step "Restarting all services" +systemctl restart nginx || handle_error "Failed to restart Nginx" +systemctl restart php${PHP_VERSION}-fpm || handle_error "Failed to restart PHP ${PHP_VERSION} FPM" +systemctl restart supervisor || handle_error "Failed to restart Supervisor" +systemctl restart openvpn || handle_error "Failed to restart OpenVPN" +COMPLETED_STEPS+=("Services restarted") -# Run the script once to apply initial configuration -/usr/local/bin/update_memory_config.sh || handle_error "Failed to run initial memory configuration" -COMPLETED_STEPS+=("Automatic memory configuration script installed at /usr/local/bin/update_memory_config.sh") - -# Final verification -log_step "Verifying all services are running" -for service in nginx mariadb freeradius valkey-server php${PHP_VERSION}-fpm; do - if ! systemctl is-active --quiet $service; then - log_warning "$service is not running" - systemctl status $service - else - log_success "$service is running" +# Open Firewall Ports and enable ufw +log_step "Opening firewall ports and enabling ufw" +ufw allow ssh || handle_error "Failed to allow SSH through firewall" +ufw allow 9080/tcp || handle_error "Failed to allow port 9080 through firewall" +ufw allow http || handle_error "Failed to allow HTTP through firewall" +ufw allow https || handle_error "Failed to allow HTTPS through firewall" +ufw allow 1194/tcp || handle_error "Failed to allow OpenVPN through firewall" +ufw allow 1812:1813/udp || handle_error "Failed to allow FreeRADIUS through firewall" +ufw reload || handle_error "Failed to reload firewall rules" +yes | ufw enable || handle_error "Failed to enable firewall" +COMPLETED_STEPS+=("Firewall ports opened and ufw enabled") + +# Configure SSL with Certbot +log_step "Configuring SSL with Certbot" +echo "Configuring SSL certificate for $DOMAIN" + +# Check if SSL certificate already exists for this domain +if [ -d "/etc/letsencrypt/live/$DOMAIN" ]; then + log_info "SSL certificate already exists for $DOMAIN, configuring it in Nginx" + # Configure existing certificate in Nginx + certbot --nginx -d "$DOMAIN" --agree-tos --email "$EMAIL_ADDRESS" --no-eff-email --non-interactive --redirect || handle_error "Failed to configure existing SSL certificate in Nginx" + COMPLETED_STEPS+=("SSL certificate configured in Nginx (existing certificate)") +else + log_info "No existing SSL certificate found for $DOMAIN, requesting new certificate" + certbot --nginx -d "$DOMAIN" --agree-tos --email "$EMAIL_ADDRESS" --no-eff-email --non-interactive --redirect || handle_error "Failed to configure SSL with Certbot" + COMPLETED_STEPS+=("SSL configured with Certbot") +fi + +# Create cleanup script for uninstalling all software +log_step "Creating cleanup script" +cat > /root/clean_server.sh << 'EOL' +#!/bin/bash + +# Cleanup script for SimpleISP/SimpleSpot +# This script will uninstall all software installed by the SimpleISP/SimpleSpot installer +# and clean the server for reinstallation + +echo "[$(date)] Starting cleanup process..." + +# Function to handle errors +handle_error() { + echo "[$(date)] ERROR: $1" + exit 1 +} + +# Function to log steps +log_step() { + echo "[$(date)] STEP: $1" +} + +# Confirm before proceeding +echo "WARNING: This will remove ALL software installed by SimpleISP/SimpleSpot and delete all data." +echo "This action CANNOT be undone!" +read -p "Are you sure you want to proceed? (y/n): " -n 1 -r +echo +if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "Cleanup cancelled." + exit 0 +fi + +# Detect PHP version (try common versions) +PHP_VERSION="" +for version in 8.2 8.1 8.0 7.4; do + if command -v php${version} &> /dev/null; then + PHP_VERSION=$version + break fi done -# Complete installation message +if [ -z "$PHP_VERSION" ]; then + echo "Warning: Could not detect PHP version, using 8.2 as default" + PHP_VERSION="8.2" +fi + +echo "[$(date)] Detected PHP version: $PHP_VERSION" + +# Stop services +log_step "Stopping services" +systemctl stop nginx freeradius mariadb valkey php${PHP_VERSION}-fpm supervisor openvpn || echo "Could not stop all services" + +# Remove web files +log_step "Removing web files" +rm -rf /var/www/html/* 2>/dev/null +rm -rf /var/www/html/.* 2>/dev/null + +# Remove configuration directories +log_step "Removing configuration directories" +rm -rf /etc/nginx/sites-available/default 2>/dev/null +rm -rf /etc/nginx/sites-enabled/default 2>/dev/null +rm -rf /etc/freeradius 2>/dev/null +rm -rf /etc/openvpn 2>/dev/null +rm -rf /etc/supervisor 2>/dev/null + +# Remove Redis data +log_step "Removing Redis data" +rm -rf /var/lib/redis/* 2>/dev/null +rm -rf /var/lib/redis/.* 2>/dev/null + +# Remove MySQL/MariaDB data and users +log_step "Removing MySQL/MariaDB data and users" +systemctl stop mariadb 2>/dev/null || echo "MariaDB was not running" +mysql -e "DROP USER IF EXISTS 'simpleisp'@'%';" 2>/dev/null || echo "Could not remove simpleisp user" +mysql -e "DROP USER IF EXISTS 'radius'@'%';" 2>/dev/null || echo "Could not remove radius user" +mysql -e "DROP DATABASE IF EXISTS radius;" 2>/dev/null || echo "Could not remove radius database" +mysql -e "FLUSH PRIVILEGES;" 2>/dev/null + +# Remove MySQL/MariaDB files +log_step "Removing MySQL/MariaDB files" +systemctl stop mariadb 2>/dev/null || echo "MariaDB already stopped" +rm -rf /var/lib/mysql 2>/dev/null +mkdir -p /var/lib/mysql +chown mysql:mysql /var/lib/mysql +rm -rf /run/mysqld 2>/dev/null +rm -f /root/.my.cnf 2>/dev/null +rm -f /root/.mysql_history 2>/dev/null + +# Remove log files +log_step "Removing log files" +rm -f /var/log/nginx/access.log 2>/dev/null +rm -f /var/log/nginx/error.log 2>/dev/null +rm -f /var/log/freeradius/radius.log 2>/dev/null + +# Remove SSL certificates +log_step "Preserving SSL certificates (not removing for reuse on reinstall)" +# rm -rf /etc/letsencrypt/live/* 2>/dev/null +# rm -rf /etc/letsencrypt/archive/* 2>/dev/null +# rm -rf /etc/letsencrypt/renewal/* 2>/dev/null + +# Remove application-specific files +log_step "Removing application-specific files" +# Preserve db.txt for credential reuse on reinstall +# rm -f /root/db.txt 2>/dev/null +rm -f /etc/cron.d/laravel-scheduler 2>/dev/null + +# Remove ionCube files +log_step "Removing ionCube files" +rm -f /etc/php/${PHP_VERSION}/mods-available/ioncube.ini 2>/dev/null +rm -f /etc/php/${PHP_VERSION}/cli/conf.d/00-ioncube.ini 2>/dev/null +rm -f /etc/php/${PHP_VERSION}/fpm/conf.d/00-ioncube.ini 2>/dev/null +# Preserve ionCube installation for reuse on reinstall +# rm -rf /usr/local/ioncube 2>/dev/null +find /usr/lib/php/ -name "*ioncube*" -delete 2>/dev/null + +# Remove temporary files +log_step "Removing temporary files" +rm -rf /tmp/ioncube* 2>/dev/null +rm -f /tmp/*.zip 2>/dev/null +rm -f /tmp/*.tar.gz 2>/dev/null + +# Create cleanup marker +log_step "Creating cleanup marker" +date '+%Y-%m-%d %H:%M:%S' > "/root/.simpleisp_cleanup_done" +echo "[$(date)] Cleanup completed. Marker created at /root/.simpleisp_cleanup_done" + +echo "[$(date)] Cleanup completed. The server is now clean and ready for reinstallation." +EOL + +# Make the cleanup script executable +chmod +x /root/clean_server.sh || handle_error "Failed to make cleanup script executable" +log_success "Cleanup script created at /root/clean_server.sh" +COMPLETED_STEPS+=("Cleanup script created at /root/clean_server.sh") + log_success "Installation completed successfully!" echo "You can find your database credentials in $DB_CREDENTIALS_FILE" echo "Your SimpleISP installation is available at: https://$DOMAIN" +echo "Installation logs are available at: $INSTALL_LOG" +echo "To uninstall everything and clean the server, run: /root/clean_server.sh" diff --git a/ubuntu_simplespot.sh b/ubuntu_simplespot.sh index ce73d8c..9f25b98 100644 --- a/ubuntu_simplespot.sh +++ b/ubuntu_simplespot.sh @@ -14,22 +14,11 @@ GITHUB_REPO_URL="https://github.com/simpleisp/simplespot.git" PHP_VERSION="8.2" # Logging functions -log_info() { - echo "ℹ️ INFO: $1" | tee -a "$INSTALL_LOG" -} - -log_success() { - echo "✅ SUCCESS: $1" | tee -a "$INSTALL_LOG" -} - -log_error() { - echo "❌ ERROR: $1" | tee -a "$INSTALL_LOG" -} - -log_step() { - STEP_COUNT=$((STEP_COUNT + 1)) - echo "👉 STEP $STEP_COUNT: $1" | tee -a "$INSTALL_LOG" -} +log_info() { echo "ℹ️ INFO: $1" | tee -a "$INSTALL_LOG"; } +log_success() { echo "✅ SUCCESS: $1" | tee -a "$INSTALL_LOG"; } +log_error() { echo "❌ ERROR: $1" | tee -a "$INSTALL_LOG"; } +log_warning() { echo "⚠️ WARNING: $1" | tee -a "$INSTALL_LOG"; } +log_step() { STEP_COUNT=$((STEP_COUNT + 1)); echo "👉 STEP $STEP_COUNT: $1" | tee -a "$INSTALL_LOG"; } handle_error() { log_error "$1" @@ -73,9 +62,7 @@ fi # Ensure script runs as root log_step "Checking root privileges" -if [ "$EUID" -ne 0 ]; then - handle_error "Please run as root" -fi +if [ "$EUID" -ne 0 ]; then handle_error "Please run as root"; fi COMPLETED_STEPS+=("Root check passed") # Get Ubuntu version @@ -124,18 +111,10 @@ printf 'Package: /freeradius/\nPin: origin "packages.networkradius.com"\nPin-Pri # Add NetworkRADIUS repository based on Ubuntu version case $UBUNTU_VERSION in - "noble") - REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/noble noble main" - ;; - "jammy") - REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/jammy jammy main" - ;; - "focal") - REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/focal focal main" - ;; - *) - handle_error "Unsupported Ubuntu version: $UBUNTU_VERSION" - ;; + "noble") REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/noble noble main" ;; + "jammy") REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/jammy jammy main" ;; + "focal") REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/focal focal main" ;; + *) handle_error "Unsupported Ubuntu version: $UBUNTU_VERSION" ;; esac echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/packages.networkradius.com.asc] $REPO_URL" | sudo tee /etc/apt/sources.list.d/networkradius.list > /dev/null || handle_error "Failed to add NetworkRADIUS repository" @@ -151,101 +130,20 @@ apt-get update || handle_error "Failed to update package lists" apt-get upgrade -y || handle_error "Failed to upgrade packages" COMPLETED_STEPS+=("System packages updated") -# Install required packages +# Install required packages (Cleaned virtual PHP packages and freeradius-rest) log_step "Installing required packages" +INSTALL_PACKAGES="nginx-full python3-certbot-nginx php${PHP_VERSION}-fpm php${PHP_VERSION}-mysql php${PHP_VERSION}-cli php${PHP_VERSION}-curl php${PHP_VERSION}-zip php${PHP_VERSION}-common php${PHP_VERSION}-gd php${PHP_VERSION}-mbstring php${PHP_VERSION}-xml php${PHP_VERSION}-dev php${PHP_VERSION}-bcmath php${PHP_VERSION}-intl php${PHP_VERSION}-redis git unzip curl wget software-properties-common apt-transport-https ca-certificates gnupg lsb-release supervisor valkey-server valkey-tools valkey-redis-compat valkey-sentinel ufw openvpn easy-rsa freeradius freeradius-mysql freeradius-utils mariadb-server mariadb-client" + if [ "$REINSTALL" = true ]; then log_info "Reinstalling packages (forcing configuration file replacement)" - apt-get install --reinstall -y -o Dpkg::Options::="--force-confmiss" \ - nginx-full \ - python3-certbot-nginx \ - php${PHP_VERSION}-fpm \ - php${PHP_VERSION}-mysql \ - php${PHP_VERSION}-cli \ - php${PHP_VERSION}-curl \ - php${PHP_VERSION}-zip \ - php${PHP_VERSION}-common \ - php${PHP_VERSION}-gd \ - php${PHP_VERSION}-mbstring \ - php${PHP_VERSION}-xml \ - php${PHP_VERSION}-dev \ - php${PHP_VERSION}-bcmath \ - php${PHP_VERSION}-intl \ - php${PHP_VERSION}-redis \ - php${PHP_VERSION}-tokenizer \ - php${PHP_VERSION}-ctype \ - php${PHP_VERSION}-fileinfo \ - git \ - unzip \ - curl \ - wget \ - software-properties-common \ - apt-transport-https \ - ca-certificates \ - gnupg \ - lsb-release \ - supervisor \ - valkey-server \ - valkey-tools \ - valkey-redis-compat \ - valkey-sentinel \ - ufw \ - openvpn \ - easy-rsa \ - freeradius \ - freeradius-mysql \ - freeradius-utils \ - freeradius-rest \ - mariadb-server \ - mariadb-client || handle_error "Failed to reinstall packages" + apt-get install --reinstall -y -o Dpkg::Options::="--force-confmiss" $INSTALL_PACKAGES || handle_error "Failed to reinstall packages" else - apt-get install -y \ - nginx-full \ - python3-certbot-nginx \ - php${PHP_VERSION}-fpm \ - php${PHP_VERSION}-mysql \ - php${PHP_VERSION}-cli \ - php${PHP_VERSION}-curl \ - php${PHP_VERSION}-zip \ - php${PHP_VERSION}-common \ - php${PHP_VERSION}-gd \ - php${PHP_VERSION}-mbstring \ - php${PHP_VERSION}-xml \ - php${PHP_VERSION}-dev \ - php${PHP_VERSION}-bcmath \ - php${PHP_VERSION}-intl \ - php${PHP_VERSION}-redis \ - php${PHP_VERSION}-tokenizer \ - php${PHP_VERSION}-ctype \ - php${PHP_VERSION}-fileinfo \ - git \ - unzip \ - curl \ - wget \ - software-properties-common \ - apt-transport-https \ - ca-certificates \ - gnupg \ - lsb-release \ - supervisor \ - valkey-server \ - valkey-tools \ - valkey-redis-compat \ - valkey-sentinel \ - ufw \ - openvpn \ - easy-rsa \ - mariadb-server \ - mariadb-client \ - freeradius \ - freeradius-mysql \ - freeradius-utils \ - freeradius-rest || handle_error "Failed to install packages" + apt-get install -y $INSTALL_PACKAGES || handle_error "Failed to install packages" fi COMPLETED_STEPS+=("Required packages installed") # Configure Valkey service overrides log_step "Configuring Valkey service overrides" - VKEY_OVERRIDE_DIR="/etc/systemd/system/valkey.service.d" VKEY_OVERRIDE_FILE="${VKEY_OVERRIDE_DIR}/override.conf" @@ -269,19 +167,15 @@ EOF # Apply changes and restart Valkey log_info "Applying Valkey service configuration..." -# Configure Valkey with optimal settings for FreeRADIUS +# Configure Valkey with optimized settings for FreeRADIUS log_step "Configuring Valkey with optimized settings" -# Calculate optimal memory allocation (75% of available RAM, capped at 3GB, minimum 1GB) +# Calculate optimal memory allocation (Safe 15% of available RAM, capped at 3GB, minimum 1GB) TOTAL_RAM_KB=$(grep MemTotal /proc/meminfo | awk '{print $2}') TOTAL_RAM_MB=$((TOTAL_RAM_KB / 1024)) -MAX_MEMORY_MB=$((TOTAL_RAM_MB * 75 / 100)) -if [ "$MAX_MEMORY_MB" -gt 3072 ]; then - MAX_MEMORY_MB=3072 # Cap at 3GB -fi -if [ "$MAX_MEMORY_MB" -lt 1024 ]; then - MAX_MEMORY_MB=1024 # Minimum 1GB -fi +MAX_MEMORY_MB=$((TOTAL_RAM_MB * 15 / 100)) +if [ "$MAX_MEMORY_MB" -gt 3072 ]; then MAX_MEMORY_MB=3072; fi +if [ "$MAX_MEMORY_MB" -lt 1024 ]; then MAX_MEMORY_MB=1024; fi log_info "Configuring Valkey with ${MAX_MEMORY_MB}MB memory allocation" @@ -291,7 +185,6 @@ mkdir -p /etc/valkey || handle_error "Failed to create Valkey configuration dire # Configure Valkey with optimized settings for FreeRADIUS cat > /etc/valkey/valkey.conf << EOL # Valkey configuration for FreeRADIUS - bind 0.0.0.0 ::0 protected-mode yes port 6379 @@ -330,8 +223,7 @@ dbfilename dump.rdb # Disable RDB snapshots since we're using AOF save "" -# Security -# Reuse the same password as MySQL for simplicity +# Security (Reuse the same password as MySQL for simplicity) requirepass "$MYSQL_PASSWORD" # Network @@ -340,8 +232,6 @@ repl-timeout 60 repl-ping-slave-period 10 repl-backlog-size 1mb repl-backlog-ttl 3600 - -# Client timeouts timeout 0 tcp-keepalive 300 @@ -351,39 +241,23 @@ rename-command FLUSHALL "" rename-command CONFIG "" rename-command SHUTDOWN "" -# Set the number of threads to serve the requests +# Tune threads and clients io-threads 2 io-threads-do-reads yes - -# Set the max number of connected clients at the same time maxclients 10000 -# Set the threshold for keys with an expire set to be considered for deletion +# Tune data structures active-expire-effort 1 - -# Set the threshold for client output buffer limits client-output-buffer-limit normal 0 0 0 client-output-buffer-limit replica 256mb 64mb 60 client-output-buffer-limit pubsub 32mb 8mb 60 - -# Tune hash data structure hash-max-ziplist-entries 512 hash-max-ziplist-value 64 - -# Tune list data structure list-max-ziplist-size -2 - -# Tune set data structure set-max-intset-entries 512 - -# Tune zset data structure zset-max-ziplist-entries 128 zset-max-ziplist-value 64 - -# Tune hll data structure hll-sparse-max-bytes 3000 - -# Tune stream data structure stream-node-max-bytes 4096 stream-node-max-entries 100 @@ -396,22 +270,6 @@ active-defrag-cycle-max 75 active-defrag-max-scan-fields 1000 EOL -# Set proper permissions for Valkey directories -# log_step "Setting Valkey directory permissions" -# mkdir -p /var/lib/valkey/appendonlydir -# chown -R valkey:valkey /var/lib/valkey /var/log/valkey /var/run/valkey -# chmod 750 /var/lib/valkey /var/log/valkey /var/run/valkey - -# Fix Valkey service -#sed -i 's/ConditionPathExists=!\/etc\/valkey\/REDIS_MIGRATION/ConditionPathExists=\/etc\/valkey\/REDIS_MIGRATION/g' /usr/lib/systemd/system/valkey-server.service -#touch /etc/valkey/REDIS_MIGRATION - -# # Set permissions for AOF directory if it exists -# if [ -d "/var/lib/valkey/appendonlydir" ]; then -# chmod 750 /var/lib/valkey/appendonlydir -# chown -R valkey:valkey /var/lib/valkey/appendonlydir -# fi - # Restart Valkey to apply new configuration systemctl daemon-reexec || log_warning "daemon-reexec failed (non-critical)" systemctl daemon-reload || handle_error "Failed to reload systemd daemon" @@ -420,23 +278,18 @@ systemctl enable valkey-server || handle_error "Failed to enable Valkey" # Verify Valkey is running log_step "Verifying Valkey service status" - -# Check service status if systemctl is-active --quiet valkey-server; then log_success "Valkey service is running" COMPLETED_STEPS+=("Valkey configured with ${MAX_MEMORY_MB}MB memory allocation") else - # If service is not running, try to get more information log_warning "Valkey service is not running as expected. Checking status..." systemctl status valkey-server --no-pager || true - # Try to start the service log_info "Attempting to start Valkey service..." if systemctl start valkey-server; then log_success "Successfully started Valkey service" COMPLETED_STEPS+=("Valkey configured with ${MAX_MEMORY_MB}MB memory allocation") else - # If we still can't start, show detailed error but don't fail the script log_error "Failed to start Valkey service. Please check the logs with: journalctl -u valkey-server -n 50" log_warning "Continuing installation despite Valkey service issue..." COMPLETED_STEPS+=("Valkey configuration completed but service failed to start") @@ -456,52 +309,26 @@ systemctl status valkey --no-pager -l echo -e "\n=== Valkey Key Statistics ===" echo "Total Keys in DB 0: $(valkey-cli -h $VALKEY_HOST -p $VALKEY_PORT dbsize)" - EOF -chmod +x /usr/local/bin/valkey-debug.sh - +chmod +x /usr/local/bin/valkey-debug.sh || handle_error "Failed to make Valkey debug script executable" COMPLETED_STEPS+=("Valkey monitoring configured") -# Add monitoring cron job +# Add monitoring cron job (Safe Append Fix) log_step "Adding monitoring cron job" -echo "*/5 * * * * /usr/local/bin/valkey-debug.sh" > cronjob || handle_error "Failed to write monitoring cron job to temporary file" -crontab cronjob || handle_error "Failed to install monitoring cron job" -rm cronjob || handle_error "Failed to remove temporary monitoring cron job file" +(crontab -l 2>/dev/null; echo "*/5 * * * * /usr/local/bin/valkey-debug.sh") | crontab - || handle_error "Failed to install monitoring cron job" COMPLETED_STEPS+=("Monitoring cron job added") # Verify Valkey is working log_step "Verifying Valkey installation" -if ! systemctl is-active --quiet valkey; then -handle_error "Valkey service is not running" -fi +if ! systemctl is-active --quiet valkey; then handle_error "Valkey service is not running"; fi # Test Valkey connectivity and basic operations -if [ "$(valkey-cli ping)" != "PONG" ]; then -handle_error "Valkey is not responding to ping" -fi - -# Test Valkey write operation -if [ "$(valkey-cli set test_key test_value)" != "OK" ]; then -handle_error "Valkey write operation failed" -fi - -# Test Valkey read operation -TEST_VALUE=$(valkey-cli get test_key) -if [ "$TEST_VALUE" != "test_value" ]; then -handle_error "Valkey read operation failed" -fi - -# Test Valkey delete operation -if [ "$(valkey-cli del test_key)" != "1" ]; then -handle_error "Valkey delete operation failed" -fi - -# Check Valkey info for basic stats -if ! valkey-cli info | grep -q "valkey_version"; then -handle_error "Unable to get Valkey server information" -fi - +if [ "$(valkey-cli ping)" != "PONG" ]; then handle_error "Valkey is not responding to ping"; fi +if [ "$(valkey-cli set test_key test_value)" != "OK" ]; then handle_error "Valkey write operation failed"; fi +if [ "$(valkey-cli get test_key)" != "test_value" ]; then handle_error "Valkey read operation failed"; fi +if [ "$(valkey-cli del test_key)" != "1" ]; then handle_error "Valkey delete operation failed"; fi +if ! valkey-cli info | grep -q "valkey_version"; then handle_error "Unable to get Valkey server information"; fi COMPLETED_STEPS+=("Valkey functionality verified") # Set Default PHP Version @@ -511,27 +338,16 @@ COMPLETED_STEPS+=("PHP ${PHP_VERSION} set as default") # Install and configure ionCube Loader log_step "Installing ionCube Loader" - -# Check if ionCube is already installed if [ -d "/usr/local/ioncube" ] && [ -f "/usr/local/ioncube/ioncube_loader_lin_${PHP_VERSION}.so" ]; then log_info "ionCube Loader already exists, skipping download and installation" COMPLETED_STEPS+=("ionCube Loader reused (already exists)") else log_info "ionCube Loader not found, downloading and installing" - - # Change to /tmp directory for downloads cd /tmp || handle_error "Failed to change to /tmp directory" - - # Download and extract ionCube wget -O ioncube.zip "https://downloads.ioncube.com/loader_downloads/ioncube_loaders_lin_x86-64.zip" || handle_error "Failed to download ionCube" unzip -q ioncube.zip || handle_error "Failed to extract ionCube" - - # Remove existing ionCube directory if it exists rm -rf /usr/local/ioncube 2>/dev/null - - # Move the ioncube directory to /usr/local mv ioncube /usr/local/ || handle_error "Failed to move ionCube to /usr/local" - COMPLETED_STEPS+=("ionCube Loader downloaded and installed") fi @@ -543,8 +359,6 @@ EOL # Enable ionCube for PHP CLI and FPM ln -sf /etc/php/${PHP_VERSION}/mods-available/ioncube.ini /etc/php/${PHP_VERSION}/cli/conf.d/00-ioncube.ini || handle_error "Failed to enable ionCube for CLI" ln -sf /etc/php/${PHP_VERSION}/mods-available/ioncube.ini /etc/php/${PHP_VERSION}/fpm/conf.d/00-ioncube.ini || handle_error "Failed to enable ionCube for FPM" - -# Restart PHP-FPM to load ionCube systemctl restart php${PHP_VERSION}-fpm || handle_error "Failed to restart PHP-FPM" # Verify ionCube installation @@ -557,8 +371,6 @@ fi # Start and enable MariaDB log_step "Configuring MariaDB" - -# Initialize MariaDB system database if not already done if [ ! -d "/var/lib/mysql/mysql" ]; then log_info "Initializing MariaDB system database" mysql_install_db --user=mysql --datadir=/var/lib/mysql || handle_error "Failed to initialize MariaDB" @@ -572,137 +384,60 @@ if [ ! -f "/etc/mysql/debian-start" ]; then log_info "Creating missing /etc/mysql/debian-start script" cat > /etc/mysql/debian-start << 'EOF' #!/bin/bash -# This script is executed by "/etc/init.d/mysql" on every (re)start. - -# Exit if the script is not being run by root -if [ "$(id -u)" != "0" ]; then - echo "This script must be run as root" 1>&2 - exit 1 -fi - -# Exit successfully if mysql is not running -if ! /usr/bin/mysqladmin --defaults-file=/etc/mysql/debian.cnf ping > /dev/null 2>&1; then - exit 0 -fi - -# Exit successfully +if [ "$(id -u)" != "0" ]; then echo "This script must be run as root" 1>&2; exit 1; fi +if ! /usr/bin/mysqladmin --defaults-file=/etc/mysql/debian.cnf ping > /dev/null 2>&1; then exit 0; fi exit 0 EOF chmod +x /etc/mysql/debian-start || handle_error "Failed to make debian-start executable" log_success "Created /etc/mysql/debian-start script" fi - COMPLETED_STEPS+=("MariaDB initialized, started and enabled") # Configure MySQL to allow remote connections log_step "Configuring MySQL for remote connections" +mkdir -p /etc/mysql/mariadb.conf.d/ || handle_error "Failed to create MariaDB conf directory" -# Create MariaDB configuration directory if it doesn't exist -mkdir -p /etc/mysql/mariadb.conf.d/ - -# Configure MariaDB cat > /etc/mysql/mariadb.conf.d/50-server.cnf << 'EOL' -# -# These groups are read by MariaDB server. -# Use it for options that only the server (but not clients) should see - -# this is read by the standalone daemon and embedded servers [server] - -# this is only for the mariadbd daemon [mariadbd] - -# -# * Basic Settings -# - user = mysql pid-file = /run/mysqld/mysqld.pid basedir = /usr datadir = /var/lib/mysql tmpdir = /tmp - -# Broken reverse DNS slows down connections considerably and name resolve is -# safe to skip if there are no "host by domain name" access grants skip-external-locking - -# Bind to all interfaces for network access bind-address = 0.0.0.0 - -# -# * Fine Tuning -# - key_buffer_size = 16M max_allowed_packet = 16M thread_stack = 192K thread_cache_size = 8 - -# This replaces the startup script and checks MyISAM tables if needed -# the first time they are touched myisam-recover-options = BACKUP - -# -# * Query Cache Configuration -# - query_cache_limit = 1M query_cache_size = 16M - -# -# * Logging and Replication -# - -# The following can be used as easy to replay backup logs or for replication. expire_logs_days = 10 max_binlog_size = 100M - -# -# * Character sets -# - -# MariaDB default is now utf8 4-byte character set. character-set-server = utf8mb4 collation-server = utf8mb4_general_ci - -# -# * InnoDB Performance Optimizations -# - -# Most important is to give InnoDB 80 % of the system RAM for buffer use: innodb_buffer_pool_size = 1G innodb_log_file_size = 256M innodb_flush_method = O_DIRECT innodb_flush_log_at_trx_commit = 2 innodb_file_per_table = 1 - -# this is only for embedded server [embedded] - -# This group is only read by MariaDB servers, not by MySQL. -# If you use the same .cnf file for MySQL and MariaDB, -# you can put MariaDB-only options here [mariadb] - EOL -# Restart MariaDB to apply changes systemctl restart mariadb || handle_error "Failed to restart MariaDB after configuration change" COMPLETED_STEPS+=("MySQL configured for remote connections") # Generate random credentials or reuse existing ones DB_CREDENTIALS_FILE="/root/db.txt" - -# Check if db.txt exists and contains valid credentials if [ -f "$DB_CREDENTIALS_FILE" ] && [ -r "$DB_CREDENTIALS_FILE" ]; then log_step "Found existing database credentials, reusing them" - - # Extract credentials from existing db.txt file MYSQL_USER=$(grep "^DB_USERNAME=" "$DB_CREDENTIALS_FILE" | cut -d'=' -f2) MYSQL_PASSWORD=$(grep "^DB_PASSWORD=" "$DB_CREDENTIALS_FILE" | cut -d'=' -f2) MYSQL_DATABASE=$(grep "^DB_DATABASE=" "$DB_CREDENTIALS_FILE" | cut -d'=' -f2) - # Validate that we got all required credentials if [ -n "$MYSQL_USER" ] && [ -n "$MYSQL_PASSWORD" ] && [ -n "$MYSQL_DATABASE" ]; then log_info "Reusing existing database credentials: User=$MYSQL_USER, Database=$MYSQL_DATABASE" COMPLETED_STEPS+=("Database credentials reused from existing file") @@ -743,13 +478,8 @@ COMPLETED_STEPS+=("MariaDB installation secured") # Create database and user log_step "Creating database and user" mysql -e "CREATE DATABASE $MYSQL_DATABASE;" || handle_error "Failed to create database" - -# Create user with access from any host mysql -e "CREATE USER '$MYSQL_USER'@'%' IDENTIFIED BY '$MYSQL_PASSWORD';" || handle_error "Failed to create database user" - -# Grant privileges for all hosts mysql -e "GRANT ALL PRIVILEGES ON $MYSQL_DATABASE.* TO '$MYSQL_USER'@'%';" || handle_error "Failed to grant database privileges" - mysql -e "FLUSH PRIVILEGES;" || handle_error "Failed to flush MariaDB privileges" COMPLETED_STEPS+=("Database and user created with full access") @@ -766,16 +496,12 @@ COMPLETED_STEPS+=("Composer installed") # Configure Nginx log_step "Configuring Nginx" +mv /etc/nginx/sites-available/default /etc/nginx/sites-available/default.bak || handle_error "Failed to backup default Nginx site" +touch /etc/nginx/sites-available/default || handle_error "Failed to create new Nginx site" +ln -sf /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default || handle_error "Failed to symlink Nginx site" -# Backup existing default site -mv /etc/nginx/sites-available/default /etc/nginx/sites-available/default.bak -touch /etc/nginx/sites-available/default -ln -sf /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default - -# Configure Nginx cat > /etc/nginx/sites-available/default << EOL server { - listen 80; listen [::]:80; @@ -796,7 +522,6 @@ server { location ~ /\.ht { deny all; } - } EOL COMPLETED_STEPS+=("Nginx configured") @@ -805,7 +530,6 @@ COMPLETED_STEPS+=("Nginx configured") log_step "Configuring SSL with Certbot" echo "Configuring SSL certificate for $DOMAIN" -# Check if SSL certificate already exists for this domain if [ -d "/etc/letsencrypt/live/$DOMAIN" ]; then log_info "SSL certificate already exists for $DOMAIN, reusing existing certificate" echo -e "1\n" | certbot --nginx -d "$DOMAIN" || handle_error "Failed to reuse SSL certificate" @@ -816,31 +540,27 @@ else COMPLETED_STEPS+=("SSL configured with Certbot") fi - -# Test the Nginx configuration +# Test the Nginx configuration (Graceful restart applied) log_step "Restarting Nginx" nginx -t || handle_error "Nginx configuration failed" -pkill nginx || handle_error "Failed to stop Nginx" -systemctl start nginx || handle_error "Failed to start Nginx" -COMPLETED_STEPS+=("Nginx restarted") +systemctl restart nginx || handle_error "Failed to restart Nginx" +COMPLETED_STEPS+=("Nginx restarted gracefully") # Setup Laravel application log_step "Setting up Laravel application" LOCAL_PATH="/var/www/html" REPO_URL="$GITHUB_REPO_URL" -# Remove existing web root if it exists (no backup) if [ -d "$LOCAL_PATH" ]; then rm -rf "$LOCAL_PATH" || handle_error "Failed to remove existing web root" fi -# Clone the repository git clone -b master "$REPO_URL" "$LOCAL_PATH" || handle_error "Failed to clone repository" cd "$LOCAL_PATH" || handle_error "Failed to change directory to web root" # Install Laravel dependencies log_step "Installing Laravel dependencies" -composer install --no-interaction || handle_error "Failed to install Laravel dependencies" +COMPOSER_ALLOW_SUPERUSER=1 composer install --no-interaction --no-security-blocking --prefer-dist || handle_error "Failed to install Laravel dependencies" COMPLETED_STEPS+=("Laravel dependencies installed") # Create and configure .env file @@ -859,7 +579,6 @@ sed -i "s|APP_URL=.*|APP_URL=https://$DOMAIN|" .env || handle_error "Failed to u sed -i "s|APP_NAME=.*|APP_NAME=\"$DOMAIN\"|" .env || handle_error "Failed to update APP_NAME in .env" COMPLETED_STEPS+=(".env file updated with database credentials") - # Run Laravel migrations and seed the database log_step "Running Laravel migrations and seeding database" php artisan migrate --force || handle_error "Failed to run Laravel migrations" @@ -879,66 +598,26 @@ chmod +x /var/www/html/sh/restart-services.sh || handle_error "Failed to make se # Run the script once to apply initial configuration /var/www/html/sh/set_permissions.sh || handle_error "Failed to run permissions script" /var/www/html/sh/restart-services.sh || handle_error "Failed to run services script" - COMPLETED_STEPS+=("Correct www permissions set") # Optimize RADIUS database indexes log_step "Optimizing RADIUS database indexes" cat > /tmp/radius_optimize.sql << "EOL" USE radius; - --- Add indexes to improve query performance - --- radcheck -ALTER TABLE radcheck - ADD INDEX idx_username (username), - ADD INDEX idx_attribute (attribute); +ALTER TABLE radcheck ADD INDEX idx_username (username), ADD INDEX idx_attribute (attribute); ANALYZE TABLE radcheck; - --- radreply -ALTER TABLE radreply - ADD INDEX idx_username (username), - ADD INDEX idx_attribute (attribute); +ALTER TABLE radreply ADD INDEX idx_username (username), ADD INDEX idx_attribute (attribute); ANALYZE TABLE radreply; - --- radusergroup -ALTER TABLE radusergroup - ADD INDEX idx_username (username), - ADD INDEX idx_groupname (groupname); +ALTER TABLE radusergroup ADD INDEX idx_username (username), ADD INDEX idx_groupname (groupname); ANALYZE TABLE radusergroup; - --- radgroupcheck -ALTER TABLE radgroupcheck - ADD INDEX idx_groupname (groupname), - ADD INDEX idx_attribute (attribute); +ALTER TABLE radgroupcheck ADD INDEX idx_groupname (groupname), ADD INDEX idx_attribute (attribute); ANALYZE TABLE radgroupcheck; - --- radgroupreply -ALTER TABLE radgroupreply - ADD INDEX idx_groupname (groupname), - ADD INDEX idx_attribute (attribute); +ALTER TABLE radgroupreply ADD INDEX idx_groupname (groupname), ADD INDEX idx_attribute (attribute); ANALYZE TABLE radgroupreply; - --- radacct (very critical for performance) -ALTER TABLE radacct - ADD INDEX idx_username (username), - ADD INDEX idx_acctsessionid (acctsessionid), - ADD INDEX idx_framedipaddress (framedipaddress), - ADD INDEX idx_acctstarttime (acctstarttime), - ADD INDEX idx_acctstoptime (acctstoptime), - ADD INDEX idx_nasipaddress (nasipaddress), - ADD INDEX idx_calledstationid (calledstationid), - ADD INDEX idx_callingstationid (callingstationid); +ALTER TABLE radacct ADD INDEX idx_username (username), ADD INDEX idx_acctsessionid (acctsessionid), ADD INDEX idx_framedipaddress (framedipaddress), ADD INDEX idx_acctstarttime (acctstarttime), ADD INDEX idx_acctstoptime (acctstoptime), ADD INDEX idx_nasipaddress (nasipaddress), ADD INDEX idx_calledstationid (calledstationid), ADD INDEX idx_callingstationid (callingstationid); ANALYZE TABLE radacct; - --- radpostauth -ALTER TABLE radpostauth - ADD INDEX idx_username (username), - ADD INDEX idx_reply (reply), - ADD INDEX idx_authdate (authdate); +ALTER TABLE radpostauth ADD INDEX idx_username (username), ADD INDEX idx_reply (reply), ADD INDEX idx_authdate (authdate); ANALYZE TABLE radpostauth; - --- Convert tables to InnoDB and utf8mb4 (recommended for reliability and Unicode support) ALTER TABLE radcheck ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE radreply ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE radusergroup ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; @@ -946,8 +625,6 @@ ALTER TABLE radgroupcheck ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLAT ALTER TABLE radgroupreply ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE radacct ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE radpostauth ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - --- Analyze again after engine/charset conversion ANALYZE TABLE radcheck; ANALYZE TABLE radreply; ANALYZE TABLE radusergroup; @@ -955,15 +632,15 @@ ANALYZE TABLE radgroupcheck; ANALYZE TABLE radgroupreply; ANALYZE TABLE radacct; ANALYZE TABLE radpostauth; - EOL mysql -u root < /tmp/radius_optimize.sql || handle_error "Failed to optimize RADIUS database indexes" -rm -f /tmp/radius_optimize.sql +rm -f /tmp/radius_optimize.sql || handle_error "Failed to remove radius optimization sql file" COMPLETED_STEPS+=("RADIUS database indexes optimized") -# Configure Supervisor for queue worker +# Configure Supervisor for queue worker (Pre-created log dir fix) log_step "Configuring Supervisor for queue worker" +mkdir -p /var/www/html/storage/logs || handle_error "Failed to create Laravel logs directory" cat > /etc/supervisor/conf.d/queue-worker.conf << "EOL" [program:queue-worker] process_name=%(program_name)s_%(process_num)02d @@ -989,10 +666,6 @@ case $UBUNTU_VERSION in chmod +x openvpn.sh || handle_error "Failed to make OpenVPN installer executable" ./openvpn.sh || handle_error "Failed to install OpenVPN" - # Enable and start OpenVPN service - systemctl enable openvpn || handle_error "Failed to enable OpenVPN service" - systemctl start openvpn || handle_error "Failed to start OpenVPN service" - # Set more secure permissions for OpenVPN chown -R root:root /etc/openvpn || handle_error "Failed to set ownership of OpenVPN configuration directory" chmod -R 777 /etc/openvpn || handle_error "Failed to set permissions of OpenVPN configuration directory" @@ -1004,29 +677,36 @@ case $UBUNTU_VERSION in esac COMPLETED_STEPS+=("OpenVPN installed") -# Install cron -log_step "Installing cron" -# Write cron job entry to a temporary file -echo "* * * * * php /var/www/html/artisan schedule:run >> /dev/null 2>&1" > cronjob || handle_error "Failed to write cron job to temporary file" +# Configure Systemd sandbox overrides for OpenVPN writes +log_step "Configuring Systemd sandbox overrides for OpenVPN writes" +mkdir -p /etc/systemd/system/php${PHP_VERSION}-fpm.service.d || handle_error "Failed to create PHP systemd override directory" +cat > /etc/systemd/system/php${PHP_VERSION}-fpm.service.d/override.conf << 'EOF' +[Service] +ReadWritePaths=/etc/openvpn +EOF -# Install the cron job from the temporary file -crontab cronjob || handle_error "Failed to install cron job" -COMPLETED_STEPS+=("Cron job installed") +mkdir -p /etc/systemd/system/supervisor.service.d || handle_error "Failed to create Supervisor systemd override directory" +cat > /etc/systemd/system/supervisor.service.d/override.conf << 'EOF' +[Service] +ReadWritePaths=/etc/openvpn +EOF +COMPLETED_STEPS+=("Systemd sandbox overrides configured") -# Clean up the temporary file -rm cronjob || handle_error "Failed to remove temporary cron job file" -COMPLETED_STEPS+=("Temporary cron job file removed") +# Install Laravel cron (Safe Append Fix) +log_step "Installing cron" +(crontab -l 2>/dev/null; echo "* * * * * php /var/www/html/artisan schedule:run >> /dev/null 2>&1") | crontab - || handle_error "Failed to install Laravel cron job" +COMPLETED_STEPS+=("Cron job installed") -# Update sudoers for www-data user +# Update sudoers for www-data user (Updated openvpn explicit target) log_step "Updating sudoers for www-data user" cat >> /etc/sudoers << 'EOL' -www-data ALL=NOPASSWD: /bin/systemctl start openvpn -www-data ALL=NOPASSWD: /bin/systemctl stop openvpn -www-data ALL=NOPASSWD: /bin/systemctl restart openvpn -www-data ALL=NOPASSWD: /bin/systemctl status openvpn -www-data ALL=NOPASSWD: /bin/systemctl reload openvpn -www-data ALL=NOPASSWD: /bin/systemctl enable openvpn -www-data ALL=NOPASSWD: /bin/systemctl disable openvpn +www-data ALL=NOPASSWD: /bin/systemctl start openvpn@server +www-data ALL=NOPASSWD: /bin/systemctl stop openvpn@server +www-data ALL=NOPASSWD: /bin/systemctl restart openvpn@server +www-data ALL=NOPASSWD: /bin/systemctl status openvpn@server +www-data ALL=NOPASSWD: /bin/systemctl reload openvpn@server +www-data ALL=NOPASSWD: /bin/systemctl enable openvpn@server +www-data ALL=NOPASSWD: /bin/systemctl disable openvpn@server www-data ALL=NOPASSWD: /bin/systemctl start freeradius www-data ALL=NOPASSWD: /bin/systemctl stop freeradius www-data ALL=NOPASSWD: /bin/systemctl restart freeradius @@ -1062,27 +742,18 @@ COMPLETED_STEPS+=("Firewall ports opened and ufw enabled") # Test FreeRADIUS configuration log_step "Checking FreeRADIUS files" -# Ensure FreeRADIUS configuration files exist (restore if missing) if [ ! -f "/etc/freeradius/radiusd.conf" ]; then -log_info "FreeRADIUS configuration files missing, reinstalling and reconfiguring FreeRADIUS package" - -# Purge and reinstall FreeRADIUS to ensure clean configuration -apt-get purge -y freeradius freeradius-common freeradius-config 2>/dev/null || echo "FreeRADIUS not installed" -apt-get autoremove -y 2>/dev/null -apt-get install -y freeradius freeradius-mysql freeradius-rest freeradius-config || handle_error "Failed to reinstall FreeRADIUS" - -# Reconfigure the package to ensure configuration files are created -dpkg-reconfigure -f noninteractive freeradius-config 2>/dev/null || echo "Reconfigure not needed" + log_info "FreeRADIUS configuration files missing, reinstalling and reconfiguring FreeRADIUS package" + apt-get purge -y freeradius freeradius-common freeradius-config 2>/dev/null || echo "FreeRADIUS not installed" + apt-get autoremove -y 2>/dev/null + apt-get install -y freeradius freeradius-mysql freeradius-config || handle_error "Failed to reinstall FreeRADIUS" + dpkg-reconfigure -f noninteractive freeradius-config 2>/dev/null || echo "Reconfigure not needed" fi -log_info "FreeRADIUS configuration not missing, skipping reinstallation...." - -# Verify configuration file was created if [ ! -f "/etc/freeradius/radiusd.conf" ]; then -# Create a minimal radiusd.conf if still missing -log_info "Creating minimal radiusd.conf configuration" -mkdir -p /etc/freeradius -cat > /etc/freeradius/radiusd.conf << 'EOF' + log_info "Creating minimal radiusd.conf configuration" + mkdir -p /etc/freeradius || handle_error "Failed to create freeradius config dir" + cat > /etc/freeradius/radiusd.conf << 'EOF' prefix = /usr exec_prefix = ${prefix} sysconfdir = /etc @@ -1100,17 +771,13 @@ cadir = ${confdir}/certs run_dir = ${localstatedir}/run/${name} db_dir = ${raddbdir} - libdir = /usr/lib/freeradius - pidfile = ${run_dir}/${name}.pid correct_escapes = true - max_request_time = 30 cleanup_delay = 5 max_requests = 16384 - hostname_lookups = no log { @@ -1136,7 +803,6 @@ status_server = yes proxy_requests = yes $INCLUDE proxy.conf - $INCLUDE clients.conf thread pool { @@ -1149,7 +815,6 @@ auto_limit_acct = no } $INCLUDE sites-enabled/ - $INCLUDE mods-enabled/ policy { @@ -1159,43 +824,30 @@ $INCLUDE policy.d/ instantiate { } EOF -chmod 644 /etc/freeradius/radiusd.conf -log_success "Created minimal radiusd.conf configuration" + chmod 644 /etc/freeradius/radiusd.conf || handle_error "Failed to set radiusd.conf permissions" + log_success "Created minimal radiusd.conf configuration" fi -# Re-enable modules after reinstallation if [ -f "/etc/freeradius/mods-available/sql" ]; then ln -sf /etc/freeradius/mods-available/sql /etc/freeradius/mods-enabled/ || handle_error "Failed to re-enable SQL module" fi -if [ -f "/etc/freeradius/mods-available/rest" ]; then - log_info "Skipping REST module re-enable; using SQL accounting" -fi -log_success "FreeRADIUS configuration files restored" - COMPLETED_STEPS+=("Completed checking FreeRADIUS files") # Enable buffered-sql site log_step "Enabling FreeRADIUS buffered-sql site" -# Ensure the sites-enabled directory exists mkdir -p /etc/freeradius/sites-enabled || handle_error "Failed to create FreeRADIUS sites-enabled directory" ln -sf /etc/freeradius/sites-available/buffered-sql /etc/freeradius/sites-enabled/buffered-sql || handle_error "Failed to enable buffered-sql site" COMPLETED_STEPS+=("FreeRADIUS buffered-sql site enabled") # Enable SQL module for FreeRADIUS log_step "Enabling SQL module" -# Ensure the mods-enabled directory exists mkdir -p /etc/freeradius/mods-enabled || handle_error "Failed to create FreeRADIUS mods-enabled directory" ln -sf /etc/freeradius/mods-available/sql /etc/freeradius/mods-enabled/sql || handle_error "Failed to enable SQL module" COMPLETED_STEPS+=("FreeRADIUS SQL module enabled") # Write new FreeRADIUS SQL module log_step "Writing new FreeRADIUS SQL module" - SQL_FILE="/etc/freeradius/mods-available/sql" - -# Backup if it exists -#[ -f "$SQL_FILE" ] && cp "$SQL_FILE" "${SQL_FILE}.bak.$(date +%Y%m%d-%H%M%S)" - cat > "$SQL_FILE" < /tmp/new_accounting_block accounting { -# Update accounting packet by adding the CUI attribute -# cui - -# detail -# daily - -# unix - -# radutmp -# sradutmp - -# sqlippool - sql - -# if (noop) { -# ok -# } - -# pgsql-voip - exec - attr_filter.accounting_response - -# Acct-Type Status-Server { -# -# } } EOF - awk ' BEGIN { skip = 0 } /^accounting[ \t]*{/ { print_block = 1; print_file("/tmp/new_accounting_block"); skip = 1; next } @@ -1323,116 +921,52 @@ EOF close(file) } ' "$DEFAULT_SITE" > /tmp/tmp_site && mv /tmp/tmp_site "$DEFAULT_SITE" - else handle_error "Default site configuration file not found" fi - COMPLETED_STEPS+=("FreeRADIUS default site configured") +# Apply Systemd Sandbox changes from the overrides +systemctl daemon-reload || handle_error "Failed to reload systemd daemon" -# Start and enable all services -log_step "Starting and enabling all services" -systemctl start nginx || handle_error "Failed to start Nginx" -systemctl enable nginx || handle_error "Failed to enable Nginx" -systemctl start php${PHP_VERSION}-fpm || handle_error "Failed to start PHP ${PHP_VERSION} FPM" -systemctl enable php${PHP_VERSION}-fpm || handle_error "Failed to enable PHP ${PHP_VERSION} FPM" -systemctl start supervisor || handle_error "Failed to start Supervisor" -systemctl enable supervisor || handle_error "Failed to enable Supervisor" -systemctl start openvpn || handle_error "Failed to start OpenVPN" -systemctl enable openvpn || handle_error "Failed to enable OpenVPN" -systemctl start freeradius || handle_error "Failed to start FreeRADIUS" -systemctl enable freeradius || handle_error "Failed to enable FreeRADIUS" -COMPLETED_STEPS+=("All services started and enabled") - -# Restart all services to ensure proper configuration -log_step "Restarting all services" -systemctl restart nginx || handle_error "Failed to restart Nginx" -systemctl restart php${PHP_VERSION}-fpm || handle_error "Failed to restart PHP ${PHP_VERSION} FPM" -systemctl restart supervisor || handle_error "Failed to restart Supervisor" -systemctl restart openvpn || handle_error "Failed to restart OpenVPN" -systemctl restart freeradius || handle_error "Failed to restart FreeRADIUS" -COMPLETED_STEPS+=("All services restarted") - -# Create a script to update memory configurations -cat > /usr/local/bin/update_memory_config.sh << 'EOL' -#!/bin/bash -# This script updates MySQL and Valkey memory configurations based on available system memory - -# Log file -LOG_FILE="/var/log/memory_config_updates.log" -echo "[$(date)] Starting memory configuration update" >> "$LOG_FILE" - -# Function to get total system memory in MB -get_total_memory_mb() { - echo $(free -m | awk '/^Mem:/{print $2}') -} - -# Update MySQL configuration -update_mysql_config() { - local total_mem=$(get_total_memory_mb) - local innodb_buffer_pool_size=$((total_mem * 60 / 100))M - local key_buffer_size=$((total_mem * 10 / 100))M - - echo "[$(date)] Updating MySQL configuration" >> "$LOG_FILE" - echo "[$(date)] Total memory: ${total_mem}MB" >> "$LOG_FILE" - echo "[$(date)] Setting innodb_buffer_pool_size to $innodb_buffer_pool_size" >> "$LOG_FILE" - echo "[$(date)] Setting key_buffer_size to $key_buffer_size" >> "$LOG_FILE" - - # Create a temporary config file - cat > /etc/mysql/conf.d/memory.cnf << MYSQL_CFG -[mysqld] -innodb_buffer_pool_size = $innodb_buffer_pool_size -key_buffer_size = $key_buffer_size -MYSQL_CFG - - # Restart MySQL if running - if systemctl is-active --quiet mariadb; then - echo "[$(date)] Restarting MySQL to apply new configuration" >> "$LOG_FILE" - systemctl restart mariadb >> "$LOG_FILE" 2>&1 || echo "[$(date)] Failed to restart MySQL" >> "$LOG_FILE" - fi -} - -# Update Valkey configuration -update_valkey_config() { - local total_mem=$(get_total_memory_mb) - local maxmemory=$((total_mem * 20 / 100))mb # Using 20% of total memory for Valkey - - echo "[$(date)] Updating Valkey configuration" >> "$LOG_FILE" - echo "[$(date)] Setting maxmemory to $maxmemory" >> "$LOG_FILE" - - # Update Valkey config - if [ -f "/etc/valkey/valkey.conf" ]; then - sed -i "s/^maxmemory .*/maxmemory $maxmemory/" /etc/valkey/valkey.conf - - # Restart Valkey if running - if systemctl is-active --quiet valkey; then - echo "[$(date)] Restarting Valkey to apply new configuration" >> "$LOG_FILE" - systemctl restart valkey >> "$LOG_FILE" 2>&1 || echo "[$(date)] Failed to restart Valkey" >> "$LOG_FILE" - fi - else - echo "[$(date)] Valkey config file not found" >> "$LOG_FILE" - fi -} - -# Execute updates -update_mysql_config -update_valkey_config - -echo "[$(date)] Memory configuration update completed" >> "$LOG_FILE" -EOL - -# Make the script executable -chmod +x /usr/local/bin/update_memory_config.sh || handle_error "Failed to make memory config script executable" - - -# Add cron job to run at 3 AM daily -(crontab -l 2>/dev/null; echo "0 3 * * * /usr/local/bin/update_memory_config.sh") | crontab - || handle_error "Failed to add cron job" +# Enable and start all services (Consolidated Block) +log_step "Enabling and restarting all services" -# Run the script once to apply initial configuration -/usr/local/bin/update_memory_config.sh || handle_error "Failed to run initial memory configuration" +SERVICES=(nginx php${PHP_VERSION}-fpm supervisor openvpn@server freeradius valkey-server) +for service in "${SERVICES[@]}"; do + systemctl enable "$service" || handle_error "Failed to enable $service" + systemctl restart "$service" || handle_error "Failed to restart $service" +done +COMPLETED_STEPS+=("All services enabled and restarted") -COMPLETED_STEPS+=("Automatic memory configuration script installed at /usr/local/bin/update_memory_config.sh") +# Install maintenance scripts (autotune, DB cleanup, OpenVPN sandbox fix) +log_step "Installing maintenance scripts to /var/www/html/sh" +for s in universal.sh db_cleanup.sh ovpn_fix.sh; do + curl -fsSL "https://raw.githubusercontent.com/simpleisp/bash/main/${s}" -o "/var/www/html/sh/${s}" || handle_error "Failed to download ${s}" + chmod +x "/var/www/html/sh/${s}" || handle_error "Failed to make ${s} executable" +done +COMPLETED_STEPS+=("Maintenance scripts installed to /var/www/html/sh") + +# Schedule maintenance (Safe Append Fix): autotune daily at 3 AM and on every +# reboot (delayed so MariaDB/Valkey/PHP-FPM are up before live tuning starts); +# DB cleanup at 04:30 (non-interactive runs use its built-in retention defaults) +log_step "Scheduling maintenance cron jobs" +(crontab -l 2>/dev/null; echo "0 3 * * * /var/www/html/sh/universal.sh") | crontab - || handle_error "Failed to add universal.sh daily cron job" +(crontab -l 2>/dev/null; echo "@reboot sleep 120; /var/www/html/sh/universal.sh") | crontab - || handle_error "Failed to add universal.sh reboot cron job" +(crontab -l 2>/dev/null; echo "30 4 * * * /var/www/html/sh/db_cleanup.sh") | crontab - || handle_error "Failed to add db_cleanup.sh cron job" +COMPLETED_STEPS+=("Maintenance cron jobs scheduled (universal.sh 3AM + reboot, db_cleanup.sh 04:30)") + +# Run the autotune once to apply the initial configuration +log_step "Running initial system autotune (universal.sh)" +/var/www/html/sh/universal.sh || handle_error "Failed to run initial autotune (universal.sh)" +COMPLETED_STEPS+=("Initial autotune applied (universal.sh)") + +# Verify PHP-FPM/queue workers can write /etc/openvpn despite systemd sandboxing. +# Idempotent: no-ops where the static overrides above already grant access, and +# catches any additional php-fpm versions or queue-worker units. +log_step "Verifying OpenVPN write access for PHP-FPM (ovpn_fix.sh)" +/var/www/html/sh/ovpn_fix.sh || log_warning "ovpn_fix.sh reported issues; review the output above and re-run /var/www/html/sh/ovpn_fix.sh manually" +COMPLETED_STEPS+=("OpenVPN sandbox write access verified (ovpn_fix.sh)") # Final verification log_step "Verifying all services are running" diff --git a/simplespot.sh b/ubuntu_simplespot_old.sh similarity index 64% rename from simplespot.sh rename to ubuntu_simplespot_old.sh index 65f1267..ce73d8c 100644 --- a/simplespot.sh +++ b/ubuntu_simplespot_old.sh @@ -14,10 +14,22 @@ GITHUB_REPO_URL="https://github.com/simpleisp/simplespot.git" PHP_VERSION="8.2" # Logging functions -log_info() { echo "ℹ️ INFO: $1" | tee -a "$INSTALL_LOG"; } -log_success() { echo "✅ SUCCESS: $1" | tee -a "$INSTALL_LOG"; } -log_error() { echo "❌ ERROR: $1" | tee -a "$INSTALL_LOG"; } -log_step() { STEP_COUNT=$((STEP_COUNT + 1)); echo "👉 STEP $STEP_COUNT: $1" | tee -a "$INSTALL_LOG"; } +log_info() { + echo "ℹ️ INFO: $1" | tee -a "$INSTALL_LOG" +} + +log_success() { + echo "✅ SUCCESS: $1" | tee -a "$INSTALL_LOG" +} + +log_error() { + echo "❌ ERROR: $1" | tee -a "$INSTALL_LOG" +} + +log_step() { + STEP_COUNT=$((STEP_COUNT + 1)) + echo "👉 STEP $STEP_COUNT: $1" | tee -a "$INSTALL_LOG" +} handle_error() { log_error "$1" @@ -61,7 +73,9 @@ fi # Ensure script runs as root log_step "Checking root privileges" -if [ "$EUID" -ne 0 ]; then handle_error "Please run as root"; fi +if [ "$EUID" -ne 0 ]; then + handle_error "Please run as root" +fi COMPLETED_STEPS+=("Root check passed") # Get Ubuntu version @@ -110,10 +124,18 @@ printf 'Package: /freeradius/\nPin: origin "packages.networkradius.com"\nPin-Pri # Add NetworkRADIUS repository based on Ubuntu version case $UBUNTU_VERSION in - "noble") REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/noble noble main" ;; - "jammy") REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/jammy jammy main" ;; - "focal") REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/focal focal main" ;; - *) handle_error "Unsupported Ubuntu version: $UBUNTU_VERSION" ;; + "noble") + REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/noble noble main" + ;; + "jammy") + REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/jammy jammy main" + ;; + "focal") + REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/focal focal main" + ;; + *) + handle_error "Unsupported Ubuntu version: $UBUNTU_VERSION" + ;; esac echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/packages.networkradius.com.asc] $REPO_URL" | sudo tee /etc/apt/sources.list.d/networkradius.list > /dev/null || handle_error "Failed to add NetworkRADIUS repository" @@ -129,20 +151,101 @@ apt-get update || handle_error "Failed to update package lists" apt-get upgrade -y || handle_error "Failed to upgrade packages" COMPLETED_STEPS+=("System packages updated") -# Install required packages (Cleaned virtual PHP packages and freeradius-rest) +# Install required packages log_step "Installing required packages" -INSTALL_PACKAGES="nginx-full python3-certbot-nginx php${PHP_VERSION}-fpm php${PHP_VERSION}-mysql php${PHP_VERSION}-cli php${PHP_VERSION}-curl php${PHP_VERSION}-zip php${PHP_VERSION}-common php${PHP_VERSION}-gd php${PHP_VERSION}-mbstring php${PHP_VERSION}-xml php${PHP_VERSION}-dev php${PHP_VERSION}-bcmath php${PHP_VERSION}-intl php${PHP_VERSION}-redis git unzip curl wget software-properties-common apt-transport-https ca-certificates gnupg lsb-release supervisor valkey-server valkey-tools valkey-redis-compat valkey-sentinel ufw openvpn easy-rsa freeradius freeradius-mysql freeradius-utils mariadb-server mariadb-client" - if [ "$REINSTALL" = true ]; then log_info "Reinstalling packages (forcing configuration file replacement)" - apt-get install --reinstall -y -o Dpkg::Options::="--force-confmiss" $INSTALL_PACKAGES || handle_error "Failed to reinstall packages" + apt-get install --reinstall -y -o Dpkg::Options::="--force-confmiss" \ + nginx-full \ + python3-certbot-nginx \ + php${PHP_VERSION}-fpm \ + php${PHP_VERSION}-mysql \ + php${PHP_VERSION}-cli \ + php${PHP_VERSION}-curl \ + php${PHP_VERSION}-zip \ + php${PHP_VERSION}-common \ + php${PHP_VERSION}-gd \ + php${PHP_VERSION}-mbstring \ + php${PHP_VERSION}-xml \ + php${PHP_VERSION}-dev \ + php${PHP_VERSION}-bcmath \ + php${PHP_VERSION}-intl \ + php${PHP_VERSION}-redis \ + php${PHP_VERSION}-tokenizer \ + php${PHP_VERSION}-ctype \ + php${PHP_VERSION}-fileinfo \ + git \ + unzip \ + curl \ + wget \ + software-properties-common \ + apt-transport-https \ + ca-certificates \ + gnupg \ + lsb-release \ + supervisor \ + valkey-server \ + valkey-tools \ + valkey-redis-compat \ + valkey-sentinel \ + ufw \ + openvpn \ + easy-rsa \ + freeradius \ + freeradius-mysql \ + freeradius-utils \ + freeradius-rest \ + mariadb-server \ + mariadb-client || handle_error "Failed to reinstall packages" else - apt-get install -y $INSTALL_PACKAGES || handle_error "Failed to install packages" + apt-get install -y \ + nginx-full \ + python3-certbot-nginx \ + php${PHP_VERSION}-fpm \ + php${PHP_VERSION}-mysql \ + php${PHP_VERSION}-cli \ + php${PHP_VERSION}-curl \ + php${PHP_VERSION}-zip \ + php${PHP_VERSION}-common \ + php${PHP_VERSION}-gd \ + php${PHP_VERSION}-mbstring \ + php${PHP_VERSION}-xml \ + php${PHP_VERSION}-dev \ + php${PHP_VERSION}-bcmath \ + php${PHP_VERSION}-intl \ + php${PHP_VERSION}-redis \ + php${PHP_VERSION}-tokenizer \ + php${PHP_VERSION}-ctype \ + php${PHP_VERSION}-fileinfo \ + git \ + unzip \ + curl \ + wget \ + software-properties-common \ + apt-transport-https \ + ca-certificates \ + gnupg \ + lsb-release \ + supervisor \ + valkey-server \ + valkey-tools \ + valkey-redis-compat \ + valkey-sentinel \ + ufw \ + openvpn \ + easy-rsa \ + mariadb-server \ + mariadb-client \ + freeradius \ + freeradius-mysql \ + freeradius-utils \ + freeradius-rest || handle_error "Failed to install packages" fi COMPLETED_STEPS+=("Required packages installed") # Configure Valkey service overrides log_step "Configuring Valkey service overrides" + VKEY_OVERRIDE_DIR="/etc/systemd/system/valkey.service.d" VKEY_OVERRIDE_FILE="${VKEY_OVERRIDE_DIR}/override.conf" @@ -166,15 +269,19 @@ EOF # Apply changes and restart Valkey log_info "Applying Valkey service configuration..." -# Configure Valkey with optimized settings for FreeRADIUS +# Configure Valkey with optimal settings for FreeRADIUS log_step "Configuring Valkey with optimized settings" -# Calculate optimal memory allocation (Safe 15% of available RAM, capped at 3GB, minimum 1GB) +# Calculate optimal memory allocation (75% of available RAM, capped at 3GB, minimum 1GB) TOTAL_RAM_KB=$(grep MemTotal /proc/meminfo | awk '{print $2}') TOTAL_RAM_MB=$((TOTAL_RAM_KB / 1024)) -MAX_MEMORY_MB=$((TOTAL_RAM_MB * 15 / 100)) -if [ "$MAX_MEMORY_MB" -gt 3072 ]; then MAX_MEMORY_MB=3072; fi -if [ "$MAX_MEMORY_MB" -lt 1024 ]; then MAX_MEMORY_MB=1024; fi +MAX_MEMORY_MB=$((TOTAL_RAM_MB * 75 / 100)) +if [ "$MAX_MEMORY_MB" -gt 3072 ]; then + MAX_MEMORY_MB=3072 # Cap at 3GB +fi +if [ "$MAX_MEMORY_MB" -lt 1024 ]; then + MAX_MEMORY_MB=1024 # Minimum 1GB +fi log_info "Configuring Valkey with ${MAX_MEMORY_MB}MB memory allocation" @@ -184,6 +291,7 @@ mkdir -p /etc/valkey || handle_error "Failed to create Valkey configuration dire # Configure Valkey with optimized settings for FreeRADIUS cat > /etc/valkey/valkey.conf << EOL # Valkey configuration for FreeRADIUS + bind 0.0.0.0 ::0 protected-mode yes port 6379 @@ -222,7 +330,8 @@ dbfilename dump.rdb # Disable RDB snapshots since we're using AOF save "" -# Security (Reuse the same password as MySQL for simplicity) +# Security +# Reuse the same password as MySQL for simplicity requirepass "$MYSQL_PASSWORD" # Network @@ -231,6 +340,8 @@ repl-timeout 60 repl-ping-slave-period 10 repl-backlog-size 1mb repl-backlog-ttl 3600 + +# Client timeouts timeout 0 tcp-keepalive 300 @@ -240,23 +351,39 @@ rename-command FLUSHALL "" rename-command CONFIG "" rename-command SHUTDOWN "" -# Tune threads and clients +# Set the number of threads to serve the requests io-threads 2 io-threads-do-reads yes + +# Set the max number of connected clients at the same time maxclients 10000 -# Tune data structures +# Set the threshold for keys with an expire set to be considered for deletion active-expire-effort 1 + +# Set the threshold for client output buffer limits client-output-buffer-limit normal 0 0 0 client-output-buffer-limit replica 256mb 64mb 60 client-output-buffer-limit pubsub 32mb 8mb 60 + +# Tune hash data structure hash-max-ziplist-entries 512 hash-max-ziplist-value 64 + +# Tune list data structure list-max-ziplist-size -2 + +# Tune set data structure set-max-intset-entries 512 + +# Tune zset data structure zset-max-ziplist-entries 128 zset-max-ziplist-value 64 + +# Tune hll data structure hll-sparse-max-bytes 3000 + +# Tune stream data structure stream-node-max-bytes 4096 stream-node-max-entries 100 @@ -269,6 +396,22 @@ active-defrag-cycle-max 75 active-defrag-max-scan-fields 1000 EOL +# Set proper permissions for Valkey directories +# log_step "Setting Valkey directory permissions" +# mkdir -p /var/lib/valkey/appendonlydir +# chown -R valkey:valkey /var/lib/valkey /var/log/valkey /var/run/valkey +# chmod 750 /var/lib/valkey /var/log/valkey /var/run/valkey + +# Fix Valkey service +#sed -i 's/ConditionPathExists=!\/etc\/valkey\/REDIS_MIGRATION/ConditionPathExists=\/etc\/valkey\/REDIS_MIGRATION/g' /usr/lib/systemd/system/valkey-server.service +#touch /etc/valkey/REDIS_MIGRATION + +# # Set permissions for AOF directory if it exists +# if [ -d "/var/lib/valkey/appendonlydir" ]; then +# chmod 750 /var/lib/valkey/appendonlydir +# chown -R valkey:valkey /var/lib/valkey/appendonlydir +# fi + # Restart Valkey to apply new configuration systemctl daemon-reexec || log_warning "daemon-reexec failed (non-critical)" systemctl daemon-reload || handle_error "Failed to reload systemd daemon" @@ -277,18 +420,23 @@ systemctl enable valkey-server || handle_error "Failed to enable Valkey" # Verify Valkey is running log_step "Verifying Valkey service status" + +# Check service status if systemctl is-active --quiet valkey-server; then log_success "Valkey service is running" COMPLETED_STEPS+=("Valkey configured with ${MAX_MEMORY_MB}MB memory allocation") else + # If service is not running, try to get more information log_warning "Valkey service is not running as expected. Checking status..." systemctl status valkey-server --no-pager || true + # Try to start the service log_info "Attempting to start Valkey service..." if systemctl start valkey-server; then log_success "Successfully started Valkey service" COMPLETED_STEPS+=("Valkey configured with ${MAX_MEMORY_MB}MB memory allocation") else + # If we still can't start, show detailed error but don't fail the script log_error "Failed to start Valkey service. Please check the logs with: journalctl -u valkey-server -n 50" log_warning "Continuing installation despite Valkey service issue..." COMPLETED_STEPS+=("Valkey configuration completed but service failed to start") @@ -308,26 +456,52 @@ systemctl status valkey --no-pager -l echo -e "\n=== Valkey Key Statistics ===" echo "Total Keys in DB 0: $(valkey-cli -h $VALKEY_HOST -p $VALKEY_PORT dbsize)" + EOF -chmod +x /usr/local/bin/valkey-debug.sh || handle_error "Failed to make Valkey debug script executable" +chmod +x /usr/local/bin/valkey-debug.sh + COMPLETED_STEPS+=("Valkey monitoring configured") -# Add monitoring cron job (Safe Append Fix) +# Add monitoring cron job log_step "Adding monitoring cron job" -(crontab -l 2>/dev/null; echo "*/5 * * * * /usr/local/bin/valkey-debug.sh") | crontab - || handle_error "Failed to install monitoring cron job" +echo "*/5 * * * * /usr/local/bin/valkey-debug.sh" > cronjob || handle_error "Failed to write monitoring cron job to temporary file" +crontab cronjob || handle_error "Failed to install monitoring cron job" +rm cronjob || handle_error "Failed to remove temporary monitoring cron job file" COMPLETED_STEPS+=("Monitoring cron job added") # Verify Valkey is working log_step "Verifying Valkey installation" -if ! systemctl is-active --quiet valkey; then handle_error "Valkey service is not running"; fi +if ! systemctl is-active --quiet valkey; then +handle_error "Valkey service is not running" +fi # Test Valkey connectivity and basic operations -if [ "$(valkey-cli ping)" != "PONG" ]; then handle_error "Valkey is not responding to ping"; fi -if [ "$(valkey-cli set test_key test_value)" != "OK" ]; then handle_error "Valkey write operation failed"; fi -if [ "$(valkey-cli get test_key)" != "test_value" ]; then handle_error "Valkey read operation failed"; fi -if [ "$(valkey-cli del test_key)" != "1" ]; then handle_error "Valkey delete operation failed"; fi -if ! valkey-cli info | grep -q "valkey_version"; then handle_error "Unable to get Valkey server information"; fi +if [ "$(valkey-cli ping)" != "PONG" ]; then +handle_error "Valkey is not responding to ping" +fi + +# Test Valkey write operation +if [ "$(valkey-cli set test_key test_value)" != "OK" ]; then +handle_error "Valkey write operation failed" +fi + +# Test Valkey read operation +TEST_VALUE=$(valkey-cli get test_key) +if [ "$TEST_VALUE" != "test_value" ]; then +handle_error "Valkey read operation failed" +fi + +# Test Valkey delete operation +if [ "$(valkey-cli del test_key)" != "1" ]; then +handle_error "Valkey delete operation failed" +fi + +# Check Valkey info for basic stats +if ! valkey-cli info | grep -q "valkey_version"; then +handle_error "Unable to get Valkey server information" +fi + COMPLETED_STEPS+=("Valkey functionality verified") # Set Default PHP Version @@ -337,16 +511,27 @@ COMPLETED_STEPS+=("PHP ${PHP_VERSION} set as default") # Install and configure ionCube Loader log_step "Installing ionCube Loader" + +# Check if ionCube is already installed if [ -d "/usr/local/ioncube" ] && [ -f "/usr/local/ioncube/ioncube_loader_lin_${PHP_VERSION}.so" ]; then log_info "ionCube Loader already exists, skipping download and installation" COMPLETED_STEPS+=("ionCube Loader reused (already exists)") else log_info "ionCube Loader not found, downloading and installing" + + # Change to /tmp directory for downloads cd /tmp || handle_error "Failed to change to /tmp directory" + + # Download and extract ionCube wget -O ioncube.zip "https://downloads.ioncube.com/loader_downloads/ioncube_loaders_lin_x86-64.zip" || handle_error "Failed to download ionCube" unzip -q ioncube.zip || handle_error "Failed to extract ionCube" + + # Remove existing ionCube directory if it exists rm -rf /usr/local/ioncube 2>/dev/null + + # Move the ioncube directory to /usr/local mv ioncube /usr/local/ || handle_error "Failed to move ionCube to /usr/local" + COMPLETED_STEPS+=("ionCube Loader downloaded and installed") fi @@ -358,6 +543,8 @@ EOL # Enable ionCube for PHP CLI and FPM ln -sf /etc/php/${PHP_VERSION}/mods-available/ioncube.ini /etc/php/${PHP_VERSION}/cli/conf.d/00-ioncube.ini || handle_error "Failed to enable ionCube for CLI" ln -sf /etc/php/${PHP_VERSION}/mods-available/ioncube.ini /etc/php/${PHP_VERSION}/fpm/conf.d/00-ioncube.ini || handle_error "Failed to enable ionCube for FPM" + +# Restart PHP-FPM to load ionCube systemctl restart php${PHP_VERSION}-fpm || handle_error "Failed to restart PHP-FPM" # Verify ionCube installation @@ -370,6 +557,8 @@ fi # Start and enable MariaDB log_step "Configuring MariaDB" + +# Initialize MariaDB system database if not already done if [ ! -d "/var/lib/mysql/mysql" ]; then log_info "Initializing MariaDB system database" mysql_install_db --user=mysql --datadir=/var/lib/mysql || handle_error "Failed to initialize MariaDB" @@ -383,60 +572,137 @@ if [ ! -f "/etc/mysql/debian-start" ]; then log_info "Creating missing /etc/mysql/debian-start script" cat > /etc/mysql/debian-start << 'EOF' #!/bin/bash -if [ "$(id -u)" != "0" ]; then echo "This script must be run as root" 1>&2; exit 1; fi -if ! /usr/bin/mysqladmin --defaults-file=/etc/mysql/debian.cnf ping > /dev/null 2>&1; then exit 0; fi +# This script is executed by "/etc/init.d/mysql" on every (re)start. + +# Exit if the script is not being run by root +if [ "$(id -u)" != "0" ]; then + echo "This script must be run as root" 1>&2 + exit 1 +fi + +# Exit successfully if mysql is not running +if ! /usr/bin/mysqladmin --defaults-file=/etc/mysql/debian.cnf ping > /dev/null 2>&1; then + exit 0 +fi + +# Exit successfully exit 0 EOF chmod +x /etc/mysql/debian-start || handle_error "Failed to make debian-start executable" log_success "Created /etc/mysql/debian-start script" fi + COMPLETED_STEPS+=("MariaDB initialized, started and enabled") # Configure MySQL to allow remote connections log_step "Configuring MySQL for remote connections" -mkdir -p /etc/mysql/mariadb.conf.d/ || handle_error "Failed to create MariaDB conf directory" +# Create MariaDB configuration directory if it doesn't exist +mkdir -p /etc/mysql/mariadb.conf.d/ + +# Configure MariaDB cat > /etc/mysql/mariadb.conf.d/50-server.cnf << 'EOL' +# +# These groups are read by MariaDB server. +# Use it for options that only the server (but not clients) should see + +# this is read by the standalone daemon and embedded servers [server] + +# this is only for the mariadbd daemon [mariadbd] + +# +# * Basic Settings +# + user = mysql pid-file = /run/mysqld/mysqld.pid basedir = /usr datadir = /var/lib/mysql tmpdir = /tmp + +# Broken reverse DNS slows down connections considerably and name resolve is +# safe to skip if there are no "host by domain name" access grants skip-external-locking + +# Bind to all interfaces for network access bind-address = 0.0.0.0 + +# +# * Fine Tuning +# + key_buffer_size = 16M max_allowed_packet = 16M thread_stack = 192K thread_cache_size = 8 + +# This replaces the startup script and checks MyISAM tables if needed +# the first time they are touched myisam-recover-options = BACKUP + +# +# * Query Cache Configuration +# + query_cache_limit = 1M query_cache_size = 16M + +# +# * Logging and Replication +# + +# The following can be used as easy to replay backup logs or for replication. expire_logs_days = 10 max_binlog_size = 100M + +# +# * Character sets +# + +# MariaDB default is now utf8 4-byte character set. character-set-server = utf8mb4 collation-server = utf8mb4_general_ci + +# +# * InnoDB Performance Optimizations +# + +# Most important is to give InnoDB 80 % of the system RAM for buffer use: innodb_buffer_pool_size = 1G innodb_log_file_size = 256M innodb_flush_method = O_DIRECT innodb_flush_log_at_trx_commit = 2 innodb_file_per_table = 1 + +# this is only for embedded server [embedded] + +# This group is only read by MariaDB servers, not by MySQL. +# If you use the same .cnf file for MySQL and MariaDB, +# you can put MariaDB-only options here [mariadb] + EOL +# Restart MariaDB to apply changes systemctl restart mariadb || handle_error "Failed to restart MariaDB after configuration change" COMPLETED_STEPS+=("MySQL configured for remote connections") # Generate random credentials or reuse existing ones DB_CREDENTIALS_FILE="/root/db.txt" + +# Check if db.txt exists and contains valid credentials if [ -f "$DB_CREDENTIALS_FILE" ] && [ -r "$DB_CREDENTIALS_FILE" ]; then log_step "Found existing database credentials, reusing them" + + # Extract credentials from existing db.txt file MYSQL_USER=$(grep "^DB_USERNAME=" "$DB_CREDENTIALS_FILE" | cut -d'=' -f2) MYSQL_PASSWORD=$(grep "^DB_PASSWORD=" "$DB_CREDENTIALS_FILE" | cut -d'=' -f2) MYSQL_DATABASE=$(grep "^DB_DATABASE=" "$DB_CREDENTIALS_FILE" | cut -d'=' -f2) + # Validate that we got all required credentials if [ -n "$MYSQL_USER" ] && [ -n "$MYSQL_PASSWORD" ] && [ -n "$MYSQL_DATABASE" ]; then log_info "Reusing existing database credentials: User=$MYSQL_USER, Database=$MYSQL_DATABASE" COMPLETED_STEPS+=("Database credentials reused from existing file") @@ -477,8 +743,13 @@ COMPLETED_STEPS+=("MariaDB installation secured") # Create database and user log_step "Creating database and user" mysql -e "CREATE DATABASE $MYSQL_DATABASE;" || handle_error "Failed to create database" + +# Create user with access from any host mysql -e "CREATE USER '$MYSQL_USER'@'%' IDENTIFIED BY '$MYSQL_PASSWORD';" || handle_error "Failed to create database user" + +# Grant privileges for all hosts mysql -e "GRANT ALL PRIVILEGES ON $MYSQL_DATABASE.* TO '$MYSQL_USER'@'%';" || handle_error "Failed to grant database privileges" + mysql -e "FLUSH PRIVILEGES;" || handle_error "Failed to flush MariaDB privileges" COMPLETED_STEPS+=("Database and user created with full access") @@ -495,12 +766,16 @@ COMPLETED_STEPS+=("Composer installed") # Configure Nginx log_step "Configuring Nginx" -mv /etc/nginx/sites-available/default /etc/nginx/sites-available/default.bak || handle_error "Failed to backup default Nginx site" -touch /etc/nginx/sites-available/default || handle_error "Failed to create new Nginx site" -ln -sf /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default || handle_error "Failed to symlink Nginx site" +# Backup existing default site +mv /etc/nginx/sites-available/default /etc/nginx/sites-available/default.bak +touch /etc/nginx/sites-available/default +ln -sf /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default + +# Configure Nginx cat > /etc/nginx/sites-available/default << EOL server { + listen 80; listen [::]:80; @@ -521,6 +796,7 @@ server { location ~ /\.ht { deny all; } + } EOL COMPLETED_STEPS+=("Nginx configured") @@ -529,6 +805,7 @@ COMPLETED_STEPS+=("Nginx configured") log_step "Configuring SSL with Certbot" echo "Configuring SSL certificate for $DOMAIN" +# Check if SSL certificate already exists for this domain if [ -d "/etc/letsencrypt/live/$DOMAIN" ]; then log_info "SSL certificate already exists for $DOMAIN, reusing existing certificate" echo -e "1\n" | certbot --nginx -d "$DOMAIN" || handle_error "Failed to reuse SSL certificate" @@ -539,21 +816,25 @@ else COMPLETED_STEPS+=("SSL configured with Certbot") fi -# Test the Nginx configuration (Graceful restart applied) + +# Test the Nginx configuration log_step "Restarting Nginx" nginx -t || handle_error "Nginx configuration failed" -systemctl restart nginx || handle_error "Failed to restart Nginx" -COMPLETED_STEPS+=("Nginx restarted gracefully") +pkill nginx || handle_error "Failed to stop Nginx" +systemctl start nginx || handle_error "Failed to start Nginx" +COMPLETED_STEPS+=("Nginx restarted") # Setup Laravel application log_step "Setting up Laravel application" LOCAL_PATH="/var/www/html" REPO_URL="$GITHUB_REPO_URL" +# Remove existing web root if it exists (no backup) if [ -d "$LOCAL_PATH" ]; then rm -rf "$LOCAL_PATH" || handle_error "Failed to remove existing web root" fi +# Clone the repository git clone -b master "$REPO_URL" "$LOCAL_PATH" || handle_error "Failed to clone repository" cd "$LOCAL_PATH" || handle_error "Failed to change directory to web root" @@ -578,6 +859,7 @@ sed -i "s|APP_URL=.*|APP_URL=https://$DOMAIN|" .env || handle_error "Failed to u sed -i "s|APP_NAME=.*|APP_NAME=\"$DOMAIN\"|" .env || handle_error "Failed to update APP_NAME in .env" COMPLETED_STEPS+=(".env file updated with database credentials") + # Run Laravel migrations and seed the database log_step "Running Laravel migrations and seeding database" php artisan migrate --force || handle_error "Failed to run Laravel migrations" @@ -597,26 +879,66 @@ chmod +x /var/www/html/sh/restart-services.sh || handle_error "Failed to make se # Run the script once to apply initial configuration /var/www/html/sh/set_permissions.sh || handle_error "Failed to run permissions script" /var/www/html/sh/restart-services.sh || handle_error "Failed to run services script" + COMPLETED_STEPS+=("Correct www permissions set") # Optimize RADIUS database indexes log_step "Optimizing RADIUS database indexes" cat > /tmp/radius_optimize.sql << "EOL" USE radius; -ALTER TABLE radcheck ADD INDEX idx_username (username), ADD INDEX idx_attribute (attribute); + +-- Add indexes to improve query performance + +-- radcheck +ALTER TABLE radcheck + ADD INDEX idx_username (username), + ADD INDEX idx_attribute (attribute); ANALYZE TABLE radcheck; -ALTER TABLE radreply ADD INDEX idx_username (username), ADD INDEX idx_attribute (attribute); + +-- radreply +ALTER TABLE radreply + ADD INDEX idx_username (username), + ADD INDEX idx_attribute (attribute); ANALYZE TABLE radreply; -ALTER TABLE radusergroup ADD INDEX idx_username (username), ADD INDEX idx_groupname (groupname); + +-- radusergroup +ALTER TABLE radusergroup + ADD INDEX idx_username (username), + ADD INDEX idx_groupname (groupname); ANALYZE TABLE radusergroup; -ALTER TABLE radgroupcheck ADD INDEX idx_groupname (groupname), ADD INDEX idx_attribute (attribute); + +-- radgroupcheck +ALTER TABLE radgroupcheck + ADD INDEX idx_groupname (groupname), + ADD INDEX idx_attribute (attribute); ANALYZE TABLE radgroupcheck; -ALTER TABLE radgroupreply ADD INDEX idx_groupname (groupname), ADD INDEX idx_attribute (attribute); + +-- radgroupreply +ALTER TABLE radgroupreply + ADD INDEX idx_groupname (groupname), + ADD INDEX idx_attribute (attribute); ANALYZE TABLE radgroupreply; -ALTER TABLE radacct ADD INDEX idx_username (username), ADD INDEX idx_acctsessionid (acctsessionid), ADD INDEX idx_framedipaddress (framedipaddress), ADD INDEX idx_acctstarttime (acctstarttime), ADD INDEX idx_acctstoptime (acctstoptime), ADD INDEX idx_nasipaddress (nasipaddress), ADD INDEX idx_calledstationid (calledstationid), ADD INDEX idx_callingstationid (callingstationid); + +-- radacct (very critical for performance) +ALTER TABLE radacct + ADD INDEX idx_username (username), + ADD INDEX idx_acctsessionid (acctsessionid), + ADD INDEX idx_framedipaddress (framedipaddress), + ADD INDEX idx_acctstarttime (acctstarttime), + ADD INDEX idx_acctstoptime (acctstoptime), + ADD INDEX idx_nasipaddress (nasipaddress), + ADD INDEX idx_calledstationid (calledstationid), + ADD INDEX idx_callingstationid (callingstationid); ANALYZE TABLE radacct; -ALTER TABLE radpostauth ADD INDEX idx_username (username), ADD INDEX idx_reply (reply), ADD INDEX idx_authdate (authdate); + +-- radpostauth +ALTER TABLE radpostauth + ADD INDEX idx_username (username), + ADD INDEX idx_reply (reply), + ADD INDEX idx_authdate (authdate); ANALYZE TABLE radpostauth; + +-- Convert tables to InnoDB and utf8mb4 (recommended for reliability and Unicode support) ALTER TABLE radcheck ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE radreply ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE radusergroup ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; @@ -624,6 +946,8 @@ ALTER TABLE radgroupcheck ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLAT ALTER TABLE radgroupreply ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE radacct ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE radpostauth ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- Analyze again after engine/charset conversion ANALYZE TABLE radcheck; ANALYZE TABLE radreply; ANALYZE TABLE radusergroup; @@ -631,15 +955,15 @@ ANALYZE TABLE radgroupcheck; ANALYZE TABLE radgroupreply; ANALYZE TABLE radacct; ANALYZE TABLE radpostauth; + EOL mysql -u root < /tmp/radius_optimize.sql || handle_error "Failed to optimize RADIUS database indexes" -rm -f /tmp/radius_optimize.sql || handle_error "Failed to remove radius optimization sql file" +rm -f /tmp/radius_optimize.sql COMPLETED_STEPS+=("RADIUS database indexes optimized") -# Configure Supervisor for queue worker (Pre-created log dir fix) +# Configure Supervisor for queue worker log_step "Configuring Supervisor for queue worker" -mkdir -p /var/www/html/storage/logs || handle_error "Failed to create Laravel logs directory" cat > /etc/supervisor/conf.d/queue-worker.conf << "EOL" [program:queue-worker] process_name=%(program_name)s_%(process_num)02d @@ -665,6 +989,10 @@ case $UBUNTU_VERSION in chmod +x openvpn.sh || handle_error "Failed to make OpenVPN installer executable" ./openvpn.sh || handle_error "Failed to install OpenVPN" + # Enable and start OpenVPN service + systemctl enable openvpn || handle_error "Failed to enable OpenVPN service" + systemctl start openvpn || handle_error "Failed to start OpenVPN service" + # Set more secure permissions for OpenVPN chown -R root:root /etc/openvpn || handle_error "Failed to set ownership of OpenVPN configuration directory" chmod -R 777 /etc/openvpn || handle_error "Failed to set permissions of OpenVPN configuration directory" @@ -676,36 +1004,29 @@ case $UBUNTU_VERSION in esac COMPLETED_STEPS+=("OpenVPN installed") -# Configure Systemd sandbox overrides for OpenVPN writes -log_step "Configuring Systemd sandbox overrides for OpenVPN writes" -mkdir -p /etc/systemd/system/php${PHP_VERSION}-fpm.service.d || handle_error "Failed to create PHP systemd override directory" -cat > /etc/systemd/system/php${PHP_VERSION}-fpm.service.d/override.conf << 'EOF' -[Service] -ReadWritePaths=/etc/openvpn -EOF - -mkdir -p /etc/systemd/system/supervisor.service.d || handle_error "Failed to create Supervisor systemd override directory" -cat > /etc/systemd/system/supervisor.service.d/override.conf << 'EOF' -[Service] -ReadWritePaths=/etc/openvpn -EOF -COMPLETED_STEPS+=("Systemd sandbox overrides configured") - -# Install Laravel cron (Safe Append Fix) +# Install cron log_step "Installing cron" -(crontab -l 2>/dev/null; echo "* * * * * php /var/www/html/artisan schedule:run >> /dev/null 2>&1") | crontab - || handle_error "Failed to install Laravel cron job" +# Write cron job entry to a temporary file +echo "* * * * * php /var/www/html/artisan schedule:run >> /dev/null 2>&1" > cronjob || handle_error "Failed to write cron job to temporary file" + +# Install the cron job from the temporary file +crontab cronjob || handle_error "Failed to install cron job" COMPLETED_STEPS+=("Cron job installed") -# Update sudoers for www-data user (Updated openvpn explicit target) +# Clean up the temporary file +rm cronjob || handle_error "Failed to remove temporary cron job file" +COMPLETED_STEPS+=("Temporary cron job file removed") + +# Update sudoers for www-data user log_step "Updating sudoers for www-data user" cat >> /etc/sudoers << 'EOL' -www-data ALL=NOPASSWD: /bin/systemctl start openvpn@server -www-data ALL=NOPASSWD: /bin/systemctl stop openvpn@server -www-data ALL=NOPASSWD: /bin/systemctl restart openvpn@server -www-data ALL=NOPASSWD: /bin/systemctl status openvpn@server -www-data ALL=NOPASSWD: /bin/systemctl reload openvpn@server -www-data ALL=NOPASSWD: /bin/systemctl enable openvpn@server -www-data ALL=NOPASSWD: /bin/systemctl disable openvpn@server +www-data ALL=NOPASSWD: /bin/systemctl start openvpn +www-data ALL=NOPASSWD: /bin/systemctl stop openvpn +www-data ALL=NOPASSWD: /bin/systemctl restart openvpn +www-data ALL=NOPASSWD: /bin/systemctl status openvpn +www-data ALL=NOPASSWD: /bin/systemctl reload openvpn +www-data ALL=NOPASSWD: /bin/systemctl enable openvpn +www-data ALL=NOPASSWD: /bin/systemctl disable openvpn www-data ALL=NOPASSWD: /bin/systemctl start freeradius www-data ALL=NOPASSWD: /bin/systemctl stop freeradius www-data ALL=NOPASSWD: /bin/systemctl restart freeradius @@ -741,18 +1062,27 @@ COMPLETED_STEPS+=("Firewall ports opened and ufw enabled") # Test FreeRADIUS configuration log_step "Checking FreeRADIUS files" +# Ensure FreeRADIUS configuration files exist (restore if missing) if [ ! -f "/etc/freeradius/radiusd.conf" ]; then - log_info "FreeRADIUS configuration files missing, reinstalling and reconfiguring FreeRADIUS package" - apt-get purge -y freeradius freeradius-common freeradius-config 2>/dev/null || echo "FreeRADIUS not installed" - apt-get autoremove -y 2>/dev/null - apt-get install -y freeradius freeradius-mysql freeradius-config || handle_error "Failed to reinstall FreeRADIUS" - dpkg-reconfigure -f noninteractive freeradius-config 2>/dev/null || echo "Reconfigure not needed" +log_info "FreeRADIUS configuration files missing, reinstalling and reconfiguring FreeRADIUS package" + +# Purge and reinstall FreeRADIUS to ensure clean configuration +apt-get purge -y freeradius freeradius-common freeradius-config 2>/dev/null || echo "FreeRADIUS not installed" +apt-get autoremove -y 2>/dev/null +apt-get install -y freeradius freeradius-mysql freeradius-rest freeradius-config || handle_error "Failed to reinstall FreeRADIUS" + +# Reconfigure the package to ensure configuration files are created +dpkg-reconfigure -f noninteractive freeradius-config 2>/dev/null || echo "Reconfigure not needed" fi +log_info "FreeRADIUS configuration not missing, skipping reinstallation...." + +# Verify configuration file was created if [ ! -f "/etc/freeradius/radiusd.conf" ]; then - log_info "Creating minimal radiusd.conf configuration" - mkdir -p /etc/freeradius || handle_error "Failed to create freeradius config dir" - cat > /etc/freeradius/radiusd.conf << 'EOF' +# Create a minimal radiusd.conf if still missing +log_info "Creating minimal radiusd.conf configuration" +mkdir -p /etc/freeradius +cat > /etc/freeradius/radiusd.conf << 'EOF' prefix = /usr exec_prefix = ${prefix} sysconfdir = /etc @@ -770,13 +1100,17 @@ cadir = ${confdir}/certs run_dir = ${localstatedir}/run/${name} db_dir = ${raddbdir} + libdir = /usr/lib/freeradius + pidfile = ${run_dir}/${name}.pid correct_escapes = true + max_request_time = 30 cleanup_delay = 5 max_requests = 16384 + hostname_lookups = no log { @@ -802,6 +1136,7 @@ status_server = yes proxy_requests = yes $INCLUDE proxy.conf + $INCLUDE clients.conf thread pool { @@ -814,6 +1149,7 @@ auto_limit_acct = no } $INCLUDE sites-enabled/ + $INCLUDE mods-enabled/ policy { @@ -823,30 +1159,43 @@ $INCLUDE policy.d/ instantiate { } EOF - chmod 644 /etc/freeradius/radiusd.conf || handle_error "Failed to set radiusd.conf permissions" - log_success "Created minimal radiusd.conf configuration" +chmod 644 /etc/freeradius/radiusd.conf +log_success "Created minimal radiusd.conf configuration" fi +# Re-enable modules after reinstallation if [ -f "/etc/freeradius/mods-available/sql" ]; then ln -sf /etc/freeradius/mods-available/sql /etc/freeradius/mods-enabled/ || handle_error "Failed to re-enable SQL module" fi +if [ -f "/etc/freeradius/mods-available/rest" ]; then + log_info "Skipping REST module re-enable; using SQL accounting" +fi +log_success "FreeRADIUS configuration files restored" + COMPLETED_STEPS+=("Completed checking FreeRADIUS files") # Enable buffered-sql site log_step "Enabling FreeRADIUS buffered-sql site" +# Ensure the sites-enabled directory exists mkdir -p /etc/freeradius/sites-enabled || handle_error "Failed to create FreeRADIUS sites-enabled directory" ln -sf /etc/freeradius/sites-available/buffered-sql /etc/freeradius/sites-enabled/buffered-sql || handle_error "Failed to enable buffered-sql site" COMPLETED_STEPS+=("FreeRADIUS buffered-sql site enabled") # Enable SQL module for FreeRADIUS log_step "Enabling SQL module" +# Ensure the mods-enabled directory exists mkdir -p /etc/freeradius/mods-enabled || handle_error "Failed to create FreeRADIUS mods-enabled directory" ln -sf /etc/freeradius/mods-available/sql /etc/freeradius/mods-enabled/sql || handle_error "Failed to enable SQL module" COMPLETED_STEPS+=("FreeRADIUS SQL module enabled") # Write new FreeRADIUS SQL module log_step "Writing new FreeRADIUS SQL module" + SQL_FILE="/etc/freeradius/mods-available/sql" + +# Backup if it exists +#[ -f "$SQL_FILE" ] && cp "$SQL_FILE" "${SQL_FILE}.bak.$(date +%Y%m%d-%H%M%S)" + cat > "$SQL_FILE" < /tmp/new_accounting_block accounting { +# Update accounting packet by adding the CUI attribute +# cui + +# detail +# daily + +# unix + +# radutmp +# sradutmp + +# sqlippool + sql + +# if (noop) { +# ok +# } + +# pgsql-voip + exec + attr_filter.accounting_response + +# Acct-Type Status-Server { +# +# } } EOF + awk ' BEGIN { skip = 0 } /^accounting[ \t]*{/ { print_block = 1; print_file("/tmp/new_accounting_block"); skip = 1; next } @@ -920,140 +1323,115 @@ EOF close(file) } ' "$DEFAULT_SITE" > /tmp/tmp_site && mv /tmp/tmp_site "$DEFAULT_SITE" + else handle_error "Default site configuration file not found" fi -COMPLETED_STEPS+=("FreeRADIUS default site configured") -# Apply Systemd Sandbox changes from the overrides -systemctl daemon-reload || handle_error "Failed to reload systemd daemon" +COMPLETED_STEPS+=("FreeRADIUS default site configured") -# Enable and start all services (Consolidated Block) -log_step "Enabling and restarting all services" -SERVICES=(nginx php${PHP_VERSION}-fpm supervisor openvpn@server freeradius valkey-server) -for service in "${SERVICES[@]}"; do - systemctl enable "$service" || handle_error "Failed to enable $service" - systemctl restart "$service" || handle_error "Failed to restart $service" -done -COMPLETED_STEPS+=("All services enabled and restarted") +# Start and enable all services +log_step "Starting and enabling all services" +systemctl start nginx || handle_error "Failed to start Nginx" +systemctl enable nginx || handle_error "Failed to enable Nginx" +systemctl start php${PHP_VERSION}-fpm || handle_error "Failed to start PHP ${PHP_VERSION} FPM" +systemctl enable php${PHP_VERSION}-fpm || handle_error "Failed to enable PHP ${PHP_VERSION} FPM" +systemctl start supervisor || handle_error "Failed to start Supervisor" +systemctl enable supervisor || handle_error "Failed to enable Supervisor" +systemctl start openvpn || handle_error "Failed to start OpenVPN" +systemctl enable openvpn || handle_error "Failed to enable OpenVPN" +systemctl start freeradius || handle_error "Failed to start FreeRADIUS" +systemctl enable freeradius || handle_error "Failed to enable FreeRADIUS" +COMPLETED_STEPS+=("All services started and enabled") + +# Restart all services to ensure proper configuration +log_step "Restarting all services" +systemctl restart nginx || handle_error "Failed to restart Nginx" +systemctl restart php${PHP_VERSION}-fpm || handle_error "Failed to restart PHP ${PHP_VERSION} FPM" +systemctl restart supervisor || handle_error "Failed to restart Supervisor" +systemctl restart openvpn || handle_error "Failed to restart OpenVPN" +systemctl restart freeradius || handle_error "Failed to restart FreeRADIUS" +COMPLETED_STEPS+=("All services restarted") # Create a script to update memory configurations cat > /usr/local/bin/update_memory_config.sh << 'EOL' -#!/usr/bin/env bash -# Optimized Co-hosted Tuning Script for SimpleSpot -# Balances MariaDB, Valkey, PHP-FPM, and FreeRADIUS. - -set -euo pipefail +#!/bin/bash +# This script updates MySQL and Valkey memory configurations based on available system memory +# Log file LOG_FILE="/var/log/memory_config_updates.log" -log() { echo "[$(date +'%F %T')] $1" >> "$LOG_FILE"; } - -log "Starting memory configuration update" - -# 1. Introspect System Resources -TOTAL_RAM_MB=$(free -m | awk '/^Mem:/{print $2}') -VCPUS=$(nproc --all) +echo "[$(date)] Starting memory configuration update" >> "$LOG_FILE" -log "Detected RAM: ${TOTAL_RAM_MB}MB | CPU Cores: ${VCPUS}" - -# 2. Budget Memory -# OS Reserve: 15% or 1024MB, whichever is higher -OS_RESERVE_MB=$(( TOTAL_RAM_MB * 15 / 100 )) -[ "$OS_RESERVE_MB" -lt 1024 ] && OS_RESERVE_MB=1024 - -# MariaDB: 40% of total RAM -MARIADB_POOL_MB=$(( TOTAL_RAM_MB * 40 / 100 )) -[ "$MARIADB_POOL_MB" -lt 256 ] && MARIADB_POOL_MB=256 - -# Valkey/Redis: 15% of total RAM -VALKEY_MB=$(( TOTAL_RAM_MB * 15 / 100 )) -[ "$VALKEY_MB" -lt 128 ] && VALKEY_MB=128 - -# PHP-FPM: The remaining RAM -REMAINING_MB=$(( TOTAL_RAM_MB - OS_RESERVE_MB - MARIADB_POOL_MB - VALKEY_MB )) -[ "$REMAINING_MB" -lt 256 ] && REMAINING_MB=256 - -log "Budget -> OS: ${OS_RESERVE_MB}MB | DB: ${MARIADB_POOL_MB}MB | Cache: ${VALKEY_MB}MB | PHP: ${REMAINING_MB}MB" +# Function to get total system memory in MB +get_total_memory_mb() { + echo $(free -m | awk '/^Mem:/{print $2}') +} -# 3. Apply MariaDB Tuning -MARIADB_CONF="/etc/mysql/mariadb.conf.d/99-autotune.cnf" -log "Tuning MariaDB..." -cat > "$MARIADB_CONF" <> "$LOG_FILE" + echo "[$(date)] Total memory: ${total_mem}MB" >> "$LOG_FILE" + echo "[$(date)] Setting innodb_buffer_pool_size to $innodb_buffer_pool_size" >> "$LOG_FILE" + echo "[$(date)] Setting key_buffer_size to $key_buffer_size" >> "$LOG_FILE" + + # Create a temporary config file + cat > /etc/mysql/conf.d/memory.cnf << MYSQL_CFG [mysqld] -innodb_buffer_pool_size = ${MARIADB_POOL_MB}M -innodb_buffer_pool_instances = $(( MARIADB_POOL_MB > 1024 ? MARIADB_POOL_MB / 1024 : 1 )) -innodb_log_file_size = $(( MARIADB_POOL_MB / 4 ))M -max_connections = $(( VCPUS * 150 )) -tmp_table_size = 64M -max_heap_table_size = 64M -EOF -systemctl restart mariadb || service mysql restart - -# 4. Apply Valkey Tuning -VALKEY_CONF="/etc/valkey/valkey.conf" -if [ -f "$VALKEY_CONF" ]; then - log "Tuning Valkey..." - # Replace existing maxmemory line or append it - sed -i -E "s/^#?maxmemory .*/maxmemory ${VALKEY_MB}mb/" "$VALKEY_CONF" - # Ensure eviction policy is set safely for Laravel (protects queues/sessions without expirations) - if ! grep -q "^maxmemory-policy" "$VALKEY_CONF"; then - echo "maxmemory-policy volatile-lru" >> "$VALKEY_CONF" - fi - systemctl restart valkey-server || true -fi - -# 5. Apply PHP-FPM Tuning -# Assuming Laravel average process size = 80MB -AVG_PHP_RSS_MB=80 -MAX_CHILDREN=$(( REMAINING_MB / AVG_PHP_RSS_MB )) -[ "$MAX_CHILDREN" -lt 5 ] && MAX_CHILDREN=5 - -# Find active PHP-FPM pool file -POOL_CONF=$(find /etc/php/*/fpm/pool.d/ -name "www.conf" | head -n 1) -if [ -n "$POOL_CONF" ]; then - log "Tuning PHP-FPM in $POOL_CONF..." - sed -i -E "s/^[;#]?pm.max_children =.*/pm.max_children = ${MAX_CHILDREN}/" "$POOL_CONF" - sed -i -E "s/^[;#]?pm.start_servers =.*/pm.start_servers = $(( MAX_CHILDREN * 20 / 100 + 1 ))/" "$POOL_CONF" - sed -i -E "s/^[;#]?pm.min_spare_servers =.*/pm.min_spare_servers = $(( MAX_CHILDREN * 10 / 100 + 1 ))/" "$POOL_CONF" - sed -i -E "s/^[;#]?pm.max_spare_servers =.*/pm.max_spare_servers = $(( MAX_CHILDREN * 30 / 100 + 1 ))/" "$POOL_CONF" +innodb_buffer_pool_size = $innodb_buffer_pool_size +key_buffer_size = $key_buffer_size +MYSQL_CFG - # Reload PHP-FPM - PHP_SVC=$(basename $(dirname $(dirname "$POOL_CONF")) | sed 's|^|php|;s|$|-fpm|') - systemctl reload "$PHP_SVC" || systemctl restart "$PHP_SVC" -fi + # Restart MySQL if running + if systemctl is-active --quiet mariadb; then + echo "[$(date)] Restarting MySQL to apply new configuration" >> "$LOG_FILE" + systemctl restart mariadb >> "$LOG_FILE" 2>&1 || echo "[$(date)] Failed to restart MySQL" >> "$LOG_FILE" + fi +} -# 6. Apply FreeRADIUS Tuning -RADIUS_CONF=$(find /etc/freeradius -name "radiusd.conf" 2>/dev/null | head -n 1) -if [ -n "$RADIUS_CONF" ]; then - log "Tuning FreeRADIUS in $RADIUS_CONF..." - # Start 2 threads per core, max out at 6 per core - START_SERVERS=$(( VCPUS * 2 )) - MAX_SERVERS=$(( VCPUS * 6 )) +# Update Valkey configuration +update_valkey_config() { + local total_mem=$(get_total_memory_mb) + local maxmemory=$((total_mem * 20 / 100))mb # Using 20% of total memory for Valkey - awk -v s="$START_SERVERS" -v m="$MAX_SERVERS" ' - BEGIN { inpool=0 } - /^thread pool \{/ { inpool=1; print; next } - /^\}/ { inpool=0; print; next } - inpool && /^[[:space:]]*start_servers/ { printf "\tstart_servers = %s\n", s; next } - inpool && /^[[:space:]]*max_servers/ { printf "\tmax_servers = %s\n", m; next } - { print } - ' "$RADIUS_CONF" > "${RADIUS_CONF}.tmp" && mv "${RADIUS_CONF}.tmp" "$RADIUS_CONF" + echo "[$(date)] Updating Valkey configuration" >> "$LOG_FILE" + echo "[$(date)] Setting maxmemory to $maxmemory" >> "$LOG_FILE" - systemctl restart freeradius || true -fi + # Update Valkey config + if [ -f "/etc/valkey/valkey.conf" ]; then + sed -i "s/^maxmemory .*/maxmemory $maxmemory/" /etc/valkey/valkey.conf + + # Restart Valkey if running + if systemctl is-active --quiet valkey; then + echo "[$(date)] Restarting Valkey to apply new configuration" >> "$LOG_FILE" + systemctl restart valkey >> "$LOG_FILE" 2>&1 || echo "[$(date)] Failed to restart Valkey" >> "$LOG_FILE" + fi + else + echo "[$(date)] Valkey config file not found" >> "$LOG_FILE" + fi +} + +# Execute updates +update_mysql_config +update_valkey_config -log "Memory configuration update completed" +echo "[$(date)] Memory configuration update completed" >> "$LOG_FILE" EOL # Make the script executable chmod +x /usr/local/bin/update_memory_config.sh || handle_error "Failed to make memory config script executable" -# Add cron job to run at 3 AM daily (Safe Append Fix applied) + +# Add cron job to run at 3 AM daily (crontab -l 2>/dev/null; echo "0 3 * * * /usr/local/bin/update_memory_config.sh") | crontab - || handle_error "Failed to add cron job" # Run the script once to apply initial configuration /usr/local/bin/update_memory_config.sh || handle_error "Failed to run initial memory configuration" + COMPLETED_STEPS+=("Automatic memory configuration script installed at /usr/local/bin/update_memory_config.sh") # Final verification @@ -1070,4 +1448,4 @@ done # Complete installation message log_success "Installation completed successfully!" echo "You can find your database credentials in $DB_CREDENTIALS_FILE" -echo "Your SimpleSpot installation is available at: https://$DOMAIN" \ No newline at end of file +echo "Your SimpleSpot installation is available at: https://$DOMAIN" diff --git a/universal.sh b/universal.sh index ce02185..e292af9 100644 --- a/universal.sh +++ b/universal.sh @@ -1,11 +1,24 @@ -#!/bin/bash +#!/bin/bash # --------------------------------------------------------------------------------- -# universal2_replaced_complete.sh — Universal autotune & in-place PHP-FPM edits +# universal2_replaced_complete.sh - Universal autotune & in-place PHP-FPM edits # - Backups, MariaDB tuning fragment, index ensures, FreeRADIUS tuning # - PHP-FPM: REPLACE existing pm.* and slowlog/status/catch directives IN-PLACE # - Removes any existing AUTOTUNE block before edits # - Safe --dry-run support, lock to avoid concurrent runs # - Heavily commented where important (you requested comments) +# +# v2 (2026-07-14): +# - No more restart-everything: MariaDB & Valkey tuned LIVE (SET GLOBAL / +# valkey-cli), PHP-FPM reloaded only if its pool changed (after php-fpm -t), +# FreeRADIUS restarted only if radiusd.conf changed AND passes -XC validation +# - Backup retention now keeps the newest KEEP runs (was: newest 3 files, +# which deleted same-run backups and mis-sorted cp -a preserved mtimes) +# - Dropped innodb_buffer_pool_instances (ignored since MariaDB 10.5), +# max_connections right-sized to observed load, old run logs pruned +# +# v3 (2026-07-14): CAPACITY MODEL - one sizing block scales every component +# from 2 vCPU / 2 GB to 16 vCPU / 16 GB (~5,000 concurrent users at top +# tier). Copy this script unchanged to any size server; it self-sizes. # --------------------------------------------------------------------------------- set -euo pipefail @@ -70,6 +83,9 @@ done mkdir -p "${DB_BACKUP_DIR}" "${CONF_BACKUP_DIR}" "$(dirname "${LOG}")" exec 3>&1 1>>"${LOG}" 2>&1 +# Prune old per-run logs so /var/log does not fill up over time +find /var/log -maxdepth 1 -name 'universal_*.log' -mtime +30 -delete 2>/dev/null || true + # Acquire an exclusive lock to avoid concurrent runs LOCK_FD=200 LOCK_FILE="/var/lock/universal.lock" @@ -104,6 +120,89 @@ log "Dry-run mode: ${DRY_RUN}" log "Backups retention (keep): ${KEEP}" log "" +# --------------------------------------------------------------- +# Legacy cleanup: update_memory_config.sh is no longer part of the +# stack (this script replaces it). Remove its file and cron entry +# if a previous install left them behind. No-op on clean machines. +# --------------------------------------------------------------- +if [ -f /usr/local/bin/update_memory_config.sh ]; then + if [ "$DRY_RUN" -eq 0 ]; then + rm -f /usr/local/bin/update_memory_config.sh || true + log "[OK] removed legacy /usr/local/bin/update_memory_config.sh" + else + log "[DRY] would remove legacy /usr/local/bin/update_memory_config.sh" + fi +fi +if crontab -l 2>/dev/null | grep -q 'update_memory_config\.sh'; then + if [ "$DRY_RUN" -eq 0 ]; then + crontab -l 2>/dev/null | grep -v 'update_memory_config\.sh' | crontab - || true + log "[OK] removed legacy update_memory_config.sh cron entry" + else + log "[DRY] would remove legacy update_memory_config.sh cron entry" + fi +fi + +# ===================================================================== +# CAPACITY MODEL (v3, 2026-07-14) - single source of sizing truth. +# Scales the whole stack from 2 vCPU / 2 GB to 16 vCPU / 16 GB. +# Design target at the top tier: ~5,000 concurrent hotspot users +# (3,500 measured weekend peak + headroom). +# +# RAM budget: OS 12% (min 384M) | InnoDB pool 25% (384M..4G; the radius +# DB is small - worker count, not cache, is the capacity currency here) | +# Valkey 10% (128M..2G) | remainder -> PHP-FPM at ~16 MB per worker. +# Worker RSS (~57MB) is mostly SHARED opcache; measured private cost is +# 8-20 MB. The vendor portal long-polls payment status with usleep() +# (vendor report issue #4), pinning one worker per waiting payer, so +# PHP-FPM workers are the scarce resource under load. +# +# Computed values for the full VM lineup (verify with --dry-run on deploy): +# TIER pool valkey workers rad-thr max_conn ~users +# 2C/2G 384M 199M 56 12 150 ~350 +# 4C/4G 896M 392M 118 24 202 ~1000 +# 4C/8G 1920M 794M 231 24 315 ~1500 +# 8C/8G 1920M 794M 231 48 339 ~2200 +# 8C/12G 2944M 1196M 343 48 451 ~3000 +# 12C/12G 2944M 1196M 343 72 475 ~3500 +# 12C/16G 3968M 1598M 457 72 589 ~4500 +# 16C/16G 3968M 1598M 457 96 613 ~5000 +# RAM sets worker count (payment-surge absorption); vCPUs set RADIUS +# threads and query throughput. Asymmetric tiers (4C/8G, 8C/12G, +# 12C/16G) are handled naturally: sleeping pollers cost RAM, not CPU. +# ===================================================================== +OS_RESERVE_MB=$(( RAM_MB * 12 / 100 )) +[ "$OS_RESERVE_MB" -lt 384 ] && OS_RESERVE_MB=384 + +POOL_MB=$(( RAM_MB * 25 / 100 )) +[ "$POOL_MB" -lt 384 ] && POOL_MB=384 +[ "$POOL_MB" -gt 4096 ] && POOL_MB=4096 +POOL_MB=$(( POOL_MB / 128 * 128 )) # 128M granularity +MARIADB_FOOTPRINT_MB=$(( POOL_MB * 13 / 10 )) # pool + ~30% engine overhead + +VALKEY_MB=$(( RAM_MB * 10 / 100 )) +[ "$VALKEY_MB" -lt 128 ] && VALKEY_MB=128 +[ "$VALKEY_MB" -gt 2048 ] && VALKEY_MB=2048 + +PHP_BUDGET_MB=$(( RAM_MB - OS_RESERVE_MB - MARIADB_FOOTPRINT_MB - VALKEY_MB )) +[ "$PHP_BUDGET_MB" -lt 256 ] && PHP_BUDGET_MB=256 +WORKER_COST_MB=16 +MAX_CHILDREN=$(( PHP_BUDGET_MB / WORKER_COST_MB )) +[ "$MAX_CHILDREN" -lt 32 ] && MAX_CHILDREN=32 +[ "$MAX_CHILDREN" -gt 480 ] && MAX_CHILDREN=480 + +RAD_MAX_SERVERS=$(( VCPUS * 6 )) +[ "$RAD_MAX_SERVERS" -lt 12 ] && RAD_MAX_SERVERS=12 +[ "$RAD_MAX_SERVERS" -gt 128 ] && RAD_MAX_SERVERS=128 + +# Every busy fpm worker + every RADIUS thread can hold one DB connection. +MAX_CONNECTIONS=$(( MAX_CHILDREN + RAD_MAX_SERVERS + 60 )) +[ "$MAX_CONNECTIONS" -lt 150 ] && MAX_CONNECTIONS=150 +[ "$MAX_CONNECTIONS" -gt 800 ] && MAX_CONNECTIONS=800 + +log "[MODEL] os_reserve=${OS_RESERVE_MB}M pool=${POOL_MB}M valkey=${VALKEY_MB}M php_budget=${PHP_BUDGET_MB}M" +log "[MODEL] fpm_max_children=${MAX_CHILDREN} rad_max_servers=${RAD_MAX_SERVERS} max_connections=${MAX_CONNECTIONS}" +log "" + # ------------------------------------------- # Step 2: Backups (DB, FreeRADIUS, PHP-FPM, MariaDB) # ------------------------------------------- @@ -194,23 +293,50 @@ log "" # Step 3: Enforce backup retention policy (keep last N) # ------------------------------------------------------- remove_old() { - local dir="$1" - find "$dir" -maxdepth 1 -type f -printf '%T@ %p\n' | sort -n | awk -v keep="$KEEP" '{files[NR]=$2} END{n=NR; for(i=1;i<=n-keep;i++){ if(i>0 && i<=n){ print files[i] }}}' | while read -r old; do - [ -n "$old" ] || continue - if [ "$DRY_RUN" -eq 0 ]; then rm -f "$old" || true; fi - log "[REMOVED] $old" - done || true + # Retain the newest KEEP *runs* (grouped by their _YYYYmmdd_HHMMSS stamp), + # not the newest KEEP files: each run writes several files, and cp -a + # preserves source mtimes, so sorting raw files by mtime deleted fresh + # backups while keeping stale ones. Timestamps sort correctly as strings. + local dir="$1" stamps keep_list s + stamps=$(find "$dir" -maxdepth 1 -type f -printf '%f\n' 2>/dev/null \ + | grep -oE '[0-9]{8}_[0-9]{6}' | sort -ur || true) + [ -n "$stamps" ] || return 0 + keep_list=$(printf '%s\n' "$stamps" | head -n "$KEEP") + for s in $stamps; do + if ! printf '%s\n' "$keep_list" | grep -qx "$s"; then + find "$dir" -maxdepth 1 -type f -name "*${s}*" | while read -r old; do + [ -n "$old" ] || continue + if [ "$DRY_RUN" -eq 0 ]; then rm -f "$old" || true; fi + log "[REMOVED] $old" + done || true + fi + done +} + +prune_baks() { + # Keep the newest KEEP ".bak." copies written next to a + # live config. Same string-sort trick as remove_old: the stamp sorts + # correctly lexically, so the current run's backup is always kept and the + # FreeRADIUS rollback path can never be pruned away. Without this, the + # daily + @reboot crons would grow these side-by-side backups unbounded. + local file="$1" old + find "$(dirname "$file")" -maxdepth 1 -name "$(basename "$file").bak.*" -printf '%f\n' 2>/dev/null \ + | sort -r | tail -n +"$((KEEP + 1))" | while read -r old; do + [ -n "$old" ] || continue + if [ "$DRY_RUN" -eq 0 ]; then rm -f "$(dirname "$file")/$old" || true; fi + log "[REMOVED] $(dirname "$file")/$old" + done || true } log "========================" -log " Step 3: Backup retention — keep last ${KEEP}" +log " Step 3: Backup retention - keep last ${KEEP}" log "------------------------" remove_old "${DB_BACKUP_DIR}" remove_old "${CONF_BACKUP_DIR}" log "" # ---------------------------------------------------------------------- -# Step 4: Schema adjustments — indexes and safe column modifications +# Step 4: Schema adjustments - indexes and safe column modifications # ---------------------------------------------------------------------- log "========================" log " Step 4: Ensure required indexes & column sizes on radius/vouchers" @@ -363,40 +489,32 @@ log "========================" log " Step 5: Enhanced MariaDB InnoDB tuning (buffer/log/conn/tmp)" log "------------------------" -TOTAL_MEM_KB=$(awk '/MemTotal/ {print $2}' /proc/meminfo 2>/dev/null || echo $((RAM_MB*1024))) -TOTAL_MEM_BYTES=$((TOTAL_MEM_KB * 1024)) -MIN_POOL_BYTES=$((1 * 1024 * 1024 * 1024)) -CALC_POOL_BYTES=$(awk -v m="$TOTAL_MEM_BYTES" 'BEGIN{printf("%d", m*0.40)}') -INNODB_BUFFER_POOL_SIZE_BYTES=$(( CALC_POOL_BYTES < MIN_POOL_BYTES ? MIN_POOL_BYTES : CALC_POOL_BYTES )) -ROUND_128MB=$((128 * 1024 * 1024)) -INNODB_BUFFER_POOL_SIZE_BYTES=$(( (INNODB_BUFFER_POOL_SIZE_BYTES / ROUND_128MB) * ROUND_128MB )) -if [ "$INNODB_BUFFER_POOL_SIZE_BYTES" -lt "$MIN_POOL_BYTES" ]; then INNODB_BUFFER_POOL_SIZE_BYTES=$MIN_POOL_BYTES; fi -INNODB_BUFFER_POOL_SIZE_MB=$(( INNODB_BUFFER_POOL_SIZE_BYTES / 1024 / 1024 )) +# All sizing comes from the CAPACITY MODEL block near the top. +INNODB_BUFFER_POOL_SIZE_MB=$POOL_MB +INNODB_BUFFER_POOL_SIZE_BYTES=$(( POOL_MB * 1024 * 1024 )) -INNODB_BUFFER_POOL_INSTANCES=$(( INNODB_BUFFER_POOL_SIZE_MB / 1024 )) -if [ "$INNODB_BUFFER_POOL_INSTANCES" -lt 1 ]; then INNODB_BUFFER_POOL_INSTANCES=1; fi -if [ "$INNODB_BUFFER_POOL_INSTANCES" -gt 8 ]; then INNODB_BUFFER_POOL_INSTANCES=8; fi +# NOTE: innodb_buffer_pool_instances was removed here - MariaDB 10.5+ +# uses a single buffer pool and silently ignores that variable. +# NOTE: this 10.11 build refuses to GROW the pool via SET GLOBAL (warning, +# value unchanged); shrinking works. Growth applies at the next restart. LOG_FILE_SIZE_BYTES=$(( INNODB_BUFFER_POOL_SIZE_BYTES / 4 )) MAX_LOG_BYTES=$((1 * 1024 * 1024 * 1024)) if [ "$LOG_FILE_SIZE_BYTES" -gt "$MAX_LOG_BYTES" ]; then LOG_FILE_SIZE_BYTES=$MAX_LOG_BYTES; fi ROUND_16MB=$((16 * 1024 * 1024)) INNODB_LOG_FILE_SIZE_BYTES=$(( (LOG_FILE_SIZE_BYTES / ROUND_16MB) * ROUND_16MB )) +[ "$INNODB_LOG_FILE_SIZE_BYTES" -lt $((96 * 1024 * 1024)) ] && INNODB_LOG_FILE_SIZE_BYTES=$((96 * 1024 * 1024)) INNODB_LOG_FILE_SIZE_MB=$(( INNODB_LOG_FILE_SIZE_BYTES / 1024 / 1024 )) -MAX_CONNECTIONS=$(( VCPUS * 150 )) -if [ "$MAX_CONNECTIONS" -gt 2000 ]; then MAX_CONNECTIONS=2000; fi - TMP_TABLE_SIZE_MB=$(( INNODB_BUFFER_POOL_SIZE_MB / 8 )) -if [ "$TMP_TABLE_SIZE_MB" -lt 64 ]; then TMP_TABLE_SIZE_MB=64; fi -if [ "$TMP_TABLE_SIZE_MB" -gt 1024 ]; then TMP_TABLE_SIZE_MB=1024; fi +if [ "$TMP_TABLE_SIZE_MB" -lt 32 ]; then TMP_TABLE_SIZE_MB=32; fi +if [ "$TMP_TABLE_SIZE_MB" -gt 512 ]; then TMP_TABLE_SIZE_MB=512; fi INNODB_IO_THREADS=$(( VCPUS < 4 ? 4 : VCPUS )) INNODB_READ_IO_THREADS=$INNODB_IO_THREADS INNODB_WRITE_IO_THREADS=$INNODB_IO_THREADS log "[INFO] Buffer pool size: ${INNODB_BUFFER_POOL_SIZE_MB}M" -log "[INFO] Buffer pool instances: ${INNODB_BUFFER_POOL_INSTANCES}" log "[INFO] Log file size: ${INNODB_LOG_FILE_SIZE_MB}M" log "[INFO] Max connections: ${MAX_CONNECTIONS}" log "[INFO] tmp_table_size/max_heap_table_size: ${TMP_TABLE_SIZE_MB}M" @@ -405,6 +523,7 @@ log "[INFO] InnoDB IO threads (read/write): ${INNODB_READ_IO_THREADS}/${INNODB_W if [ -f "${MARIADB_FRAG}" ]; then log "[RUN] backup existing MariaDB fragment -> ${MARIADB_FRAG}.bak.${TIMESTAMP}" [ "$DRY_RUN" -eq 0 ] && cp -a "${MARIADB_FRAG}" "${MARIADB_FRAG}.bak.${TIMESTAMP}" + prune_baks "${MARIADB_FRAG}" fi MARIADB_FRAG_TMP="${MARIADB_FRAG}.tmp" @@ -412,7 +531,6 @@ cat > "${MARIADB_FRAG_TMP}" <>"${LOG}"; then + log "[OK] dynamic MariaDB settings applied live -- no restart required" + else + log "[WARN] live SET GLOBAL failed; settings apply at next MariaDB restart" + fi + log "[INFO] innodb_read/write_io_threads are static and apply at the next planned restart" else log "[DRY] would write MariaDB config fragment -> ${MARIADB_FRAG}" + log "[DRY] would apply dynamic settings live via SET GLOBAL (no restart)" rm -f "${MARIADB_FRAG_TMP}" || true fi @@ -447,20 +584,36 @@ log "------------------------" VALKEY_CONF="/etc/valkey/valkey.conf" if [ -f "$VALKEY_CONF" ]; then - VALKEY_MB=$(( RAM_MB * 15 / 100 )) - [ "$VALKEY_MB" -lt 128 ] && VALKEY_MB=128 - + # VALKEY_MB comes from the CAPACITY MODEL block. log "[INFO] Valkey maxmemory: ${VALKEY_MB}M" if [ "$DRY_RUN" -eq 0 ]; then cp -a "$VALKEY_CONF" "${VALKEY_CONF}.bak.${TIMESTAMP}" + prune_baks "$VALKEY_CONF" sed -i -E "s/^#?maxmemory .*/maxmemory ${VALKEY_MB}mb/" "$VALKEY_CONF" if ! grep -q "^maxmemory-policy" "$VALKEY_CONF"; then echo "maxmemory-policy volatile-lru" >> "$VALKEY_CONF" fi log "[OK] Valkey configured with maxmemory ${VALKEY_MB}mb and volatile-lru" + + # Apply live via valkey-cli so no restart (and no cache flush) is needed; + # the config file edit above keeps the value across future restarts. + # NOTE: valkey-cli exits 0 even when the server replies ERR (this box + # rename-disables CONFIG), so verify the live value via INFO instead of + # trusting the exit code. + if command -v valkey-cli >/dev/null 2>&1; then + valkey-cli CONFIG SET maxmemory "${VALKEY_MB}mb" >/dev/null 2>&1 || true + valkey-cli CONFIG SET maxmemory-policy volatile-lru >/dev/null 2>&1 || true + LIVE_MM=$(valkey-cli INFO memory 2>/dev/null | tr -d '\r' | awk -F: '/^maxmemory:/{print $2}') + TARGET_MM=$(( VALKEY_MB * 1024 * 1024 )) + if [ "${LIVE_MM:-0}" = "${TARGET_MM}" ]; then + log "[OK] Valkey live maxmemory verified at ${VALKEY_MB}mb -- no restart required" + else + log "[WARN] Valkey live maxmemory=${LIVE_MM:-unknown} (target ${TARGET_MM}); CONFIG is disabled here -- new value applies at next Valkey restart" + fi + fi else - log "[DRY] would tune Valkey maxmemory to ${VALKEY_MB}mb and volatile-lru" + log "[DRY] would tune Valkey maxmemory to ${VALKEY_MB}mb and volatile-lru (live via valkey-cli, no restart)" fi else log "[WARN] Valkey config not found at $VALKEY_CONF, skipping" @@ -484,16 +637,18 @@ for p in "${RADIUS_PATHS[@]}"; do fi done +RADIUS_CHANGED=0 if [ -z "${RADIUS_CONF}" ]; then log "[WARN] radiusd.conf not found in common paths; skipping thread-pool edit." else START_SERVERS=$(( VCPUS / 2 )); [ "$START_SERVERS" -lt 2 ] && START_SERVERS=2 - MAX_SERVERS=$(( VCPUS * 6 )) + MAX_SERVERS=$RAD_MAX_SERVERS # from the CAPACITY MODEL ([12..128]) MIN_SPARE=$(( VCPUS / 2 )); [ "$MIN_SPARE" -lt 2 ] && MIN_SPARE=2 MAX_SPARE=$(( VCPUS * 2 )); [ "$MAX_SPARE" -lt "$MIN_SPARE" ] && MAX_SPARE=$MIN_SPARE log "[RUN] Using radiusd.conf: ${RADIUS_CONF}" [ "$DRY_RUN" -eq 0 ] && cp -a "${RADIUS_CONF}" "${RADIUS_CONF}.bak.${TIMESTAMP}" + prune_baks "${RADIUS_CONF}" TMP_RAD="${RADIUS_CONF}.new" awk -v s="${START_SERVERS}" -v m="${MAX_SERVERS}" -v minsp="${MIN_SPARE}" -v maxsp="${MAX_SPARE}" ' BEGIN{inblock=0} @@ -509,11 +664,26 @@ else ' "${RADIUS_CONF}" > "${TMP_RAD}" || true if [ "$DRY_RUN" -eq 0 ]; then - mv -f "${TMP_RAD}" "${RADIUS_CONF}" - log "[OK] radiusd.conf thread pool updated (backup: ${RADIUS_CONF}.bak.${TIMESTAMP})" + if cmp -s "${RADIUS_CONF}" "${TMP_RAD}"; then + # Nothing to do -- values already match; avoids a pointless restart. + rm -f "${TMP_RAD}" || true + log "[OK] radiusd.conf thread pool already tuned; no change, no restart needed" + else + mv -f "${TMP_RAD}" "${RADIUS_CONF}" + # Validate BEFORE deciding to restart; a broken config must never + # reach a running RADIUS service. Roll back on validation failure. + RAD_BIN="$(command -v freeradius || command -v radiusd || true)" + if [ -n "${RAD_BIN}" ] && ! "${RAD_BIN}" -XC >/dev/null 2>&1; then + log "[ERROR] new radiusd.conf FAILED validation (${RAD_BIN} -XC); restoring backup, skipping restart" + cp -a "${RADIUS_CONF}.bak.${TIMESTAMP}" "${RADIUS_CONF}" + else + RADIUS_CHANGED=1 + log "[OK] radiusd.conf thread pool updated and validated (backup: ${RADIUS_CONF}.bak.${TIMESTAMP})" + fi + fi else rm -f "${TMP_RAD}" || true - log "[DRY] would update radiusd.conf thread pool" + log "[DRY] would update radiusd.conf thread pool (validated with -XC, restart only if changed)" fi fi @@ -644,41 +814,14 @@ else # If pool file exists, compute values and do in-place replacements if [ -f "$POOL_CONF" ]; then - # Compute recommended values - CPU_CORES=$(nproc --all 2>/dev/null || echo 1) - TOTAL_RAM_MB=$(free -m | awk '/^Mem:/{print $2}') - # Match the global budget strategy: 15% OS + 40% DB + 15% Cache - OS_RESERVE_MB=$(( TOTAL_RAM_MB * 15 / 100 )) - [ "$OS_RESERVE_MB" -lt 1024 ] && OS_RESERVE_MB=1024 - MARIADB_POOL_MB=$(( TOTAL_RAM_MB * 40 / 100 )) - [ "$MARIADB_POOL_MB" -lt 256 ] && MARIADB_POOL_MB=256 - VALKEY_MB=$(( TOTAL_RAM_MB * 15 / 100 )) - [ "$VALKEY_MB" -lt 128 ] && VALKEY_MB=128 - - RESERVE_MB=$(( OS_RESERVE_MB + MARIADB_POOL_MB + VALKEY_MB )) - AVAILABLE_FOR_PHP_MB=$(( TOTAL_RAM_MB - RESERVE_MB )) - [ "$AVAILABLE_FOR_PHP_MB" -lt 256 ] && AVAILABLE_FOR_PHP_MB=256 - - # Determine avg worker RSS (best-effort) - PHPFPM_PROCNAMES=("php-fpm${PHP_VER_DETECTED}" "php${PHP_VER_DETECTED}-fpm" "php-fpm" "php7.4-fpm" "php8.1-fpm" "php8.2-fpm") - FOUND_PROCNAME="" - for pn in "${PHPFPM_PROCNAMES[@]}"; do - if pgrep -x "$pn" >/dev/null 2>&1; then FOUND_PROCNAME="$pn"; break; fi - done - AVG_RSS_KB=0 - if [ -n "$FOUND_PROCNAME" ]; then - AVG_RSS_KB=$(ps --no-headers -o rss -C "$FOUND_PROCNAME" 2>/dev/null | awk '{s+=$1;n++}END{if(n)printf "%.0f",s/n;else print 0}') - fi - if [ -z "${AVG_RSS_KB}" ] || [ "$AVG_RSS_KB" -le 0 ]; then - AVG_RSS_MB=50 - AVG_RSS_KB=$((AVG_RSS_MB * 1024)) - else - AVG_RSS_MB=$(( (AVG_RSS_KB + 1023) / 1024 )) - fi - - MAX_CHILDREN=$(( AVAILABLE_FOR_PHP_MB / AVG_RSS_MB )) - [ "$MAX_CHILDREN" -lt 2 ] && MAX_CHILDREN=2 - [ "$MAX_CHILDREN" -gt 1000 ] && MAX_CHILDREN=1000 + # Worker count comes from the CAPACITY MODEL block near the top + # (MAX_CHILDREN = PHP_BUDGET_MB / WORKER_COST_MB, clamped [32..480]). + # Do NOT size from measured RSS: worker RSS (~57MB) is mostly shared + # opcache and undercounts capacity ~4x. The vendor portal long-polls + # payment status with usleep() (vendor report issue #4), pinning one + # worker per waiting payer - worker count IS the capacity currency. + CPU_CORES=$VCPUS + TOTAL_RAM_MB=$RAM_MB PM_START=$(( MAX_CHILDREN * 20 / 100 )) PM_MIN=$(( MAX_CHILDREN * 10 / 100 )) PM_MAX=$(( MAX_CHILDREN * 30 / 100 )) @@ -702,7 +845,7 @@ else log "[DRY] would replace ${key} = ${val} in ${file}" fi else - # No existing line — insert after anchor (anchor is anchor key, e.g., 'pm' for 'pm = dynamic') + # No existing line - insert after anchor (anchor is anchor key, e.g., 'pm' for 'pm = dynamic') if [ -n "$anchor" ] && grep -qE "^[[:space:]]*${anchor}[[:space:]]*=" "$file"; then if [ "$DRY_RUN" -eq 0 ]; then awk -v a="$anchor" -v newline="${key} = ${val}" '{ @@ -742,6 +885,8 @@ else ensure_directive "$POOL_CONF" "slowlog" "${SLOWLOG_PATH}" "pm" ensure_directive "$POOL_CONF" "pm.status_path" "${STATUS_PATH}" "pm" ensure_directive "$POOL_CONF" "catch_workers_output" "${CATCH_WORKERS_OUTPUT}" "pm" + # Deeper accept queue for login bursts (default 511 drops under spikes) + ensure_directive "$POOL_CONF" "listen.backlog" "1024" "pm" if [ "$DRY_RUN" -eq 0 ]; then chmod 640 "$POOL_CONF" || true @@ -750,35 +895,44 @@ else log "[DRY] would update pool file in-place (max_children=${MAX_CHILDREN})" fi - # Reload/restart php-fpm + # Reload php-fpm ONLY if the pool file actually changed, and only after + # php-fpm's own config test passes. A failed test restores the backup. if [ "$DRY_RUN" -eq 0 ]; then - if systemctl reload "$PHPFPM_SERVICE" >/dev/null 2>&1; then - log "[OK] Reloaded $PHPFPM_SERVICE successfully." + if [ -f "$PHPFPM_POOL_BAK" ] && cmp -s "$POOL_CONF" "$PHPFPM_POOL_BAK"; then + log "[OK] pool config unchanged; skipping php-fpm reload" else - log "[WARN] Reload failed; attempting restart..." - if systemctl restart "$PHPFPM_SERVICE" >/dev/null 2>&1; then - log "[OK] Restarted $PHPFPM_SERVICE successfully." + FPM_BIN="$(command -v "php-fpm${PHP_VER_DETECTED}" || true)" + if [ -n "$FPM_BIN" ] && ! "$FPM_BIN" -t >/dev/null 2>&1; then + log "[ERROR] php-fpm config test FAILED; restoring ${PHPFPM_POOL_BAK}, skipping reload" + cp -a "$PHPFPM_POOL_BAK" "$POOL_CONF" + elif systemctl reload "$PHPFPM_SERVICE" >/dev/null 2>&1; then + log "[OK] Reloaded $PHPFPM_SERVICE successfully (config test passed)." else - log "[ERROR] Restart failed, please check $PHPFPM_SERVICE and logs manually." + log "[WARN] Reload failed; attempting restart..." + if systemctl restart "$PHPFPM_SERVICE" >/dev/null 2>&1; then + log "[OK] Restarted $PHPFPM_SERVICE successfully." + else + log "[ERROR] Restart failed, please check $PHPFPM_SERVICE and logs manually." + fi fi fi else - log "[DRY] would reload/restart $PHPFPM_SERVICE" + log "[DRY] would config-test and reload $PHPFPM_SERVICE only if pool file changed" fi # Write suggestion report cat > "$PHPFPM_SUGGESTION_FILE" </dev/null | awk '{print $1}' | grep -E '^php[0-9]+\.[0-9]+-fpm\.service$' || true) -if [ -n "$PHP_FPM_UNITS" ]; then - for u in $PHP_FPM_UNITS; do - svcname="${u%.service}" - restart_and_check "$svcname" +if [ "${RADIUS_CHANGED:-0}" -eq 1 ]; then + RAD_RESTARTED=0 + for candidate in freeradius radiusd; do + if restart_and_check "$candidate"; then + RAD_RESTARTED=1 + break + fi done + if [ "$RAD_RESTARTED" -eq 0 ]; then + log "[WARN] FreeRADIUS restart not confirmed; check OS packaging (freeradius or radiusd)" + fi else - for ver in 8.2 8.1 8.0 7.4 7.3; do - restart_and_check "php${ver}-fpm" - done + log "[OK] FreeRADIUS: config unchanged (or rolled back); no restart" fi log "" From 86416f5d0aa1a910f70ff260573e075b57447c26 Mon Sep 17 00:00:00 2001 From: Barnabie Opiyo Date: Wed, 15 Jul 2026 13:22:23 +0300 Subject: [PATCH 19/45] Add README documenting installers, maintenance scripts, and cron schedule Co-Authored-By: Claude Fable 5 --- README.md | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..52e6776 --- /dev/null +++ b/README.md @@ -0,0 +1,86 @@ +# SimpleISP / SimpleSpot Server Scripts + +Bash scripts for installing and maintaining [SimpleISP](https://github.com/simpleisp/radius) and SimpleSpot hotspot/ISP billing servers on Ubuntu (focal / jammy / noble). + +## Installers + +| Script | Installs | PHP | +|---|---|---| +| `ubuntu_simpleisp.sh` | SimpleISP (ISP billing panel) | 7.4 | +| `ubuntu_simplespot.sh` | SimpleSpot (hotspot billing panel) | 8.2 | + +The two installers are identical except for the application repository and PHP version. Run on a fresh Ubuntu server as root: + +```bash +chmod +x ubuntu_simpleisp.sh +sudo ./ubuntu_simpleisp.sh +``` + +Each installer sets up the full stack: + +- **nginx** + **PHP-FPM** serving the Laravel panel from `/var/www/html` (with Let's Encrypt via certbot) +- **MariaDB** (unix_socket root auth; credentials written to a file reported at the end of the install) +- **FreeRADIUS 3.2** (NetworkRADIUS packages) with SQL accounting into the `radius` database +- **Valkey** (Redis-compatible cache) with systemd hardening overrides +- **OpenVPN** (via `openvpn.sh`) with systemd `ReadWritePaths` overrides so the panel can manage `/etc/openvpn` +- **supervisor** for Laravel queue workers, UFW rules, cron jobs, and sudoers entries for `www-data` service control + +At the end, the installer places the maintenance scripts below into `/var/www/html/sh/`, schedules them, and runs the autotune once. + +## Maintenance scripts + +Installed to `/var/www/html/sh/` on the server; all three support `--dry-run`. + +### `universal.sh` — capacity-model autotune + +Sizes MariaDB, Valkey, PHP-FPM, and FreeRADIUS from one capacity model that scales from 2 vCPU / 2 GB to 16 vCPU / 16 GB (~5,000 concurrent hotspot users at the top tier). Copy it unchanged to any size server; it self-sizes. + +- Backs up the database and all touched configs before changing anything; keeps the newest **3** runs (including the `.bak.*` copies written next to live configs) +- Applies MariaDB and Valkey settings **live** (`SET GLOBAL` / `valkey-cli`) — no restart, no cold cache +- Reloads PHP-FPM only if its pool config changed and `php-fpm -t` passes; restarts FreeRADIUS only if `radiusd.conf` changed **and** validates with `-XC` (rolls back otherwise) +- Ensures indexes on `radacct` and friends +- Removes the legacy `update_memory_config.sh` (file + cron) if a previous install left it behind +- Logs to `/var/log/universal_.log`; backups under `/var/backups/universal/` + +### `db_cleanup.sh` — RADIUS database retention + +Batched cleanup of closed `radacct` sessions, `radpostauth`, expired portal sessions and vouchers, payment ledgers, and Laravel `failed_jobs`; closes zombie sessions and runs `ANALYZE` / conditional `OPTIMIZE`. + +``` +db_cleanup.sh [--dry-run] [--auto] [--ask] +``` + +Run interactively it prompts per category for a retention age (`12h`, `2d`, `1w`, `3m`, `s` to skip) and confirms before deleting. From cron (no terminal) it silently uses the built-in defaults. Logs to `/var/log/radius_db_cleanup.log`. + +### `ovpn_fix.sh` — PHP-FPM OpenVPN sandbox fix + +Debian/Ubuntu php-fpm units ship with `ProtectSystem=full`, which makes `/etc` read-only inside the service and breaks panel writes to `/etc/openvpn/server.conf`. This script finds every installed `phpX.Y-fpm` unit (and any systemd Laravel queue-worker units), installs a `ReadWritePaths=/etc/openvpn` drop-in, restarts the affected units, and verifies writability from inside each service's mount namespace. Idempotent — safe to re-run any time. + +``` +ovpn_fix.sh [--dry-run] [--no-restart] +``` + +## Scheduled jobs (installed to root's crontab) + +| Schedule | Job | +|---|---| +| `* * * * *` | Laravel scheduler (`artisan schedule:run`) | +| `*/5 * * * *` | Valkey health monitor (`valkey-debug.sh`) | +| `0 3 * * *` | `universal.sh` — daily autotune | +| `@reboot` (after 120 s) | `universal.sh` — re-tune after boot | +| `30 4 * * *` | `db_cleanup.sh` — database retention | + +## Utility scripts + +| Script | Purpose | +|---|---| +| `openvpn.sh` | Standalone OpenVPN road-warrior installer (downloaded and run by the installers) | +| `clean_server.sh` | Uninstalls everything the installer set up and prepares the server for a clean reinstall (writes a marker the installer detects) | +| `ports.sh` | Configures firewall/port rules for a given subnet (`ports.sh -net `) | +| `setup.sh` / `install.sh` | Minimal standalone MariaDB + app bootstrap (older path) | +| `transfer_tmpl.sh` | Proxmox: clones VM templates between nodes via the API | +| `template_generation.sh` | Proxmox: commands to build an Ubuntu cloud-image VM template | + +## Legacy + +`ubuntu_simpleisp_old.sh` and `ubuntu_simplespot_old.sh` are the previous generation of the installers, kept for reference. They embed the superseded `update_memory_config.sh` tuning approach — new installs should always use the current `ubuntu_simpleisp.sh` / `ubuntu_simplespot.sh`. From f18f3ef2e503df06138775325cc13b674fa9d632 Mon Sep 17 00:00:00 2001 From: Barnabie Opiyo Date: Wed, 15 Jul 2026 13:30:54 +0300 Subject: [PATCH 20/45] Remove db_cleanup.sh cron - install it for manual use only Co-Authored-By: Claude Fable 5 --- README.md | 5 +++-- ubuntu_simpleisp.sh | 7 +++---- ubuntu_simplespot.sh | 7 +++---- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 52e6776..7fa354e 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Batched cleanup of closed `radacct` sessions, `radpostauth`, expired portal sess db_cleanup.sh [--dry-run] [--auto] [--ask] ``` -Run interactively it prompts per category for a retention age (`12h`, `2d`, `1w`, `3m`, `s` to skip) and confirms before deleting. From cron (no terminal) it silently uses the built-in defaults. Logs to `/var/log/radius_db_cleanup.log`. +Run interactively it prompts per category for a retention age (`12h`, `2d`, `1w`, `3m`, `s` to skip) and confirms before deleting. Without a terminal (or with `--auto`) it silently uses the built-in defaults. **Not scheduled** — run it manually when the database needs trimming. Logs to `/var/log/radius_db_cleanup.log`. ### `ovpn_fix.sh` — PHP-FPM OpenVPN sandbox fix @@ -68,7 +68,8 @@ ovpn_fix.sh [--dry-run] [--no-restart] | `*/5 * * * *` | Valkey health monitor (`valkey-debug.sh`) | | `0 3 * * *` | `universal.sh` — daily autotune | | `@reboot` (after 120 s) | `universal.sh` — re-tune after boot | -| `30 4 * * *` | `db_cleanup.sh` — database retention | + +`db_cleanup.sh` is deliberately not scheduled; run it manually when needed. ## Utility scripts diff --git a/ubuntu_simpleisp.sh b/ubuntu_simpleisp.sh index bf40ab5..6f6c6c8 100644 --- a/ubuntu_simpleisp.sh +++ b/ubuntu_simpleisp.sh @@ -948,13 +948,12 @@ done COMPLETED_STEPS+=("Maintenance scripts installed to /var/www/html/sh") # Schedule maintenance (Safe Append Fix): autotune daily at 3 AM and on every -# reboot (delayed so MariaDB/Valkey/PHP-FPM are up before live tuning starts); -# DB cleanup at 04:30 (non-interactive runs use its built-in retention defaults) +# reboot (delayed so MariaDB/Valkey/PHP-FPM are up before live tuning starts). +# db_cleanup.sh is intentionally NOT scheduled - run it manually when needed. log_step "Scheduling maintenance cron jobs" (crontab -l 2>/dev/null; echo "0 3 * * * /var/www/html/sh/universal.sh") | crontab - || handle_error "Failed to add universal.sh daily cron job" (crontab -l 2>/dev/null; echo "@reboot sleep 120; /var/www/html/sh/universal.sh") | crontab - || handle_error "Failed to add universal.sh reboot cron job" -(crontab -l 2>/dev/null; echo "30 4 * * * /var/www/html/sh/db_cleanup.sh") | crontab - || handle_error "Failed to add db_cleanup.sh cron job" -COMPLETED_STEPS+=("Maintenance cron jobs scheduled (universal.sh 3AM + reboot, db_cleanup.sh 04:30)") +COMPLETED_STEPS+=("Maintenance cron jobs scheduled (universal.sh 3AM + reboot)") # Run the autotune once to apply the initial configuration log_step "Running initial system autotune (universal.sh)" diff --git a/ubuntu_simplespot.sh b/ubuntu_simplespot.sh index 9f25b98..66d6796 100644 --- a/ubuntu_simplespot.sh +++ b/ubuntu_simplespot.sh @@ -948,13 +948,12 @@ done COMPLETED_STEPS+=("Maintenance scripts installed to /var/www/html/sh") # Schedule maintenance (Safe Append Fix): autotune daily at 3 AM and on every -# reboot (delayed so MariaDB/Valkey/PHP-FPM are up before live tuning starts); -# DB cleanup at 04:30 (non-interactive runs use its built-in retention defaults) +# reboot (delayed so MariaDB/Valkey/PHP-FPM are up before live tuning starts). +# db_cleanup.sh is intentionally NOT scheduled - run it manually when needed. log_step "Scheduling maintenance cron jobs" (crontab -l 2>/dev/null; echo "0 3 * * * /var/www/html/sh/universal.sh") | crontab - || handle_error "Failed to add universal.sh daily cron job" (crontab -l 2>/dev/null; echo "@reboot sleep 120; /var/www/html/sh/universal.sh") | crontab - || handle_error "Failed to add universal.sh reboot cron job" -(crontab -l 2>/dev/null; echo "30 4 * * * /var/www/html/sh/db_cleanup.sh") | crontab - || handle_error "Failed to add db_cleanup.sh cron job" -COMPLETED_STEPS+=("Maintenance cron jobs scheduled (universal.sh 3AM + reboot, db_cleanup.sh 04:30)") +COMPLETED_STEPS+=("Maintenance cron jobs scheduled (universal.sh 3AM + reboot)") # Run the autotune once to apply the initial configuration log_step "Running initial system autotune (universal.sh)" From d120755d45b8d8d8afaa9ba3449b9ef016c2db68 Mon Sep 17 00:00:00 2001 From: Barnabie Opiyo Date: Fri, 17 Jul 2026 16:42:25 +0300 Subject: [PATCH 21/45] Fix Valkey install on focal/jammy and MariaDB config on focal Ubuntu only ships valkey packages from noble onward, so the installers failed on 20.04/22.04 with "Unable to locate package valkey-server". Restore the Percona repo path that the old installers used: focal/jammy now enable Percona's valkey repo and install valkey/valkey-compat (service "valkey"), while noble+ keeps the Ubuntu archive packages (service "valkey-server"). All systemctl calls, the systemd override dir, and the generated valkey-debug.sh now use the per-version service name - the override dir and final verification were previously hardcoded to the wrong unit on noble. Also rename the [mariadbd] section in the generated 50-server.cnf to [mysqld]: [mariadbd] only exists from MariaDB 10.4.6, so focal's 10.3 silently ignored the whole block (bind-address, InnoDB tuning, utf8mb4). Co-Authored-By: Claude Fable 5 --- README.md | 2 +- ubuntu_simpleisp.sh | 54 ++++++++++++++++++++++++++++++++------------ ubuntu_simplespot.sh | 54 ++++++++++++++++++++++++++++++++------------ 3 files changed, 79 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 7fa354e..384c9c8 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Each installer sets up the full stack: - **nginx** + **PHP-FPM** serving the Laravel panel from `/var/www/html` (with Let's Encrypt via certbot) - **MariaDB** (unix_socket root auth; credentials written to a file reported at the end of the install) - **FreeRADIUS 3.2** (NetworkRADIUS packages) with SQL accounting into the `radius` database -- **Valkey** (Redis-compatible cache) with systemd hardening overrides +- **Valkey** (Redis-compatible cache) with systemd hardening overrides — installed from Percona's repo on focal/jammy (service `valkey`), from the Ubuntu archive on noble (service `valkey-server`) - **OpenVPN** (via `openvpn.sh`) with systemd `ReadWritePaths` overrides so the panel can manage `/etc/openvpn` - **supervisor** for Laravel queue workers, UFW rules, cron jobs, and sudoers entries for `www-data` service control diff --git a/ubuntu_simpleisp.sh b/ubuntu_simpleisp.sh index 6f6c6c8..a38ac23 100644 --- a/ubuntu_simpleisp.sh +++ b/ubuntu_simpleisp.sh @@ -121,6 +121,28 @@ echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/packages.networkradius.com.asc log_success "NetworkRADIUS repository configured for Ubuntu $UBUNTU_VERSION" COMPLETED_STEPS+=("NetworkRADIUS repository configured") +# Set Valkey package source. Ubuntu ships Valkey in its own archive only from +# noble (24.04, via noble-updates backport) onward; focal/jammy get Percona's +# builds instead, which use different package and service names. +log_step "Configuring Valkey package source" +if [[ "$UBUNTU_VERSION" == "focal" || "$UBUNTU_VERSION" == "jammy" ]]; then + # Remove conflicting redis packages + apt-get remove -y redis-tools redis-server || true + + # Install Percona release package and enable its Valkey repository + curl -fsSL "https://repo.percona.com/apt/percona-release_latest.${UBUNTU_VERSION}_all.deb" -o /tmp/percona-release_latest.deb || handle_error "Failed to download percona-release package" + dpkg -i /tmp/percona-release_latest.deb || handle_error "Failed to install percona-release package" + rm -f /tmp/percona-release_latest.deb + percona-release enable valkey experimental || handle_error "Failed to enable Percona Valkey repository" + + VALKEY_PACKAGES="valkey valkey-compat" + VALKEY_SERVICE="valkey" +else + VALKEY_PACKAGES="valkey-server valkey-tools valkey-redis-compat valkey-sentinel" + VALKEY_SERVICE="valkey-server" +fi +COMPLETED_STEPS+=("Valkey package source configured (service: ${VALKEY_SERVICE})") + # Set environment variable to avoid interactive prompts export DEBIAN_FRONTEND=noninteractive @@ -132,7 +154,7 @@ COMPLETED_STEPS+=("System packages updated") # Install required packages (Cleaned virtual PHP packages and freeradius-rest) log_step "Installing required packages" -INSTALL_PACKAGES="nginx-full python3-certbot-nginx php${PHP_VERSION}-fpm php${PHP_VERSION}-mysql php${PHP_VERSION}-cli php${PHP_VERSION}-curl php${PHP_VERSION}-zip php${PHP_VERSION}-common php${PHP_VERSION}-gd php${PHP_VERSION}-mbstring php${PHP_VERSION}-xml php${PHP_VERSION}-dev php${PHP_VERSION}-bcmath php${PHP_VERSION}-intl php${PHP_VERSION}-redis git unzip curl wget software-properties-common apt-transport-https ca-certificates gnupg lsb-release supervisor valkey-server valkey-tools valkey-redis-compat valkey-sentinel ufw openvpn easy-rsa freeradius freeradius-mysql freeradius-utils mariadb-server mariadb-client" +INSTALL_PACKAGES="nginx-full python3-certbot-nginx php${PHP_VERSION}-fpm php${PHP_VERSION}-mysql php${PHP_VERSION}-cli php${PHP_VERSION}-curl php${PHP_VERSION}-zip php${PHP_VERSION}-common php${PHP_VERSION}-gd php${PHP_VERSION}-mbstring php${PHP_VERSION}-xml php${PHP_VERSION}-dev php${PHP_VERSION}-bcmath php${PHP_VERSION}-intl php${PHP_VERSION}-redis git unzip curl wget software-properties-common apt-transport-https ca-certificates gnupg lsb-release supervisor ${VALKEY_PACKAGES} ufw openvpn easy-rsa freeradius freeradius-mysql freeradius-utils mariadb-server mariadb-client" if [ "$REINSTALL" = true ]; then log_info "Reinstalling packages (forcing configuration file replacement)" @@ -144,7 +166,7 @@ COMPLETED_STEPS+=("Required packages installed") # Configure Valkey service overrides log_step "Configuring Valkey service overrides" -VKEY_OVERRIDE_DIR="/etc/systemd/system/valkey.service.d" +VKEY_OVERRIDE_DIR="/etc/systemd/system/${VALKEY_SERVICE}.service.d" VKEY_OVERRIDE_FILE="${VKEY_OVERRIDE_DIR}/override.conf" mkdir -p "$VKEY_OVERRIDE_DIR" @@ -273,24 +295,24 @@ EOL # Restart Valkey to apply new configuration systemctl daemon-reexec || log_warning "daemon-reexec failed (non-critical)" systemctl daemon-reload || handle_error "Failed to reload systemd daemon" -systemctl restart valkey-server || handle_error "Failed to restart Valkey" -systemctl enable valkey-server || handle_error "Failed to enable Valkey" +systemctl restart "$VALKEY_SERVICE" || handle_error "Failed to restart Valkey" +systemctl enable "$VALKEY_SERVICE" || handle_error "Failed to enable Valkey" # Verify Valkey is running log_step "Verifying Valkey service status" -if systemctl is-active --quiet valkey-server; then +if systemctl is-active --quiet "$VALKEY_SERVICE"; then log_success "Valkey service is running" COMPLETED_STEPS+=("Valkey configured with ${MAX_MEMORY_MB}MB memory allocation") else log_warning "Valkey service is not running as expected. Checking status..." - systemctl status valkey-server --no-pager || true + systemctl status "$VALKEY_SERVICE" --no-pager || true log_info "Attempting to start Valkey service..." - if systemctl start valkey-server; then + if systemctl start "$VALKEY_SERVICE"; then log_success "Successfully started Valkey service" COMPLETED_STEPS+=("Valkey configured with ${MAX_MEMORY_MB}MB memory allocation") else - log_error "Failed to start Valkey service. Please check the logs with: journalctl -u valkey-server -n 50" + log_error "Failed to start Valkey service. Please check the logs with: journalctl -u ${VALKEY_SERVICE} -n 50" log_warning "Continuing installation despite Valkey service issue..." COMPLETED_STEPS+=("Valkey configuration completed but service failed to start") fi @@ -298,17 +320,17 @@ fi # Create Valkey debug script log_step "Creating Valkey debug script" -cat > /usr/local/bin/valkey-debug.sh << 'EOF' +cat > /usr/local/bin/valkey-debug.sh << EOF #!/bin/bash VALKEY_HOST="127.0.0.1" VALKEY_PORT="6379" echo "=== Valkey Status ===" -systemctl status valkey --no-pager -l +systemctl status ${VALKEY_SERVICE} --no-pager -l echo -e "\n=== Valkey Key Statistics ===" -echo "Total Keys in DB 0: $(valkey-cli -h $VALKEY_HOST -p $VALKEY_PORT dbsize)" +echo "Total Keys in DB 0: \$(valkey-cli -h \$VALKEY_HOST -p \$VALKEY_PORT dbsize)" EOF chmod +x /usr/local/bin/valkey-debug.sh || handle_error "Failed to make Valkey debug script executable" @@ -321,7 +343,7 @@ COMPLETED_STEPS+=("Monitoring cron job added") # Verify Valkey is working log_step "Verifying Valkey installation" -if ! systemctl is-active --quiet valkey; then handle_error "Valkey service is not running"; fi +if ! systemctl is-active --quiet "$VALKEY_SERVICE"; then handle_error "Valkey service is not running"; fi # Test Valkey connectivity and basic operations if [ "$(valkey-cli ping)" != "PONG" ]; then handle_error "Valkey is not responding to ping"; fi @@ -399,7 +421,9 @@ mkdir -p /etc/mysql/mariadb.conf.d/ || handle_error "Failed to create MariaDB co cat > /etc/mysql/mariadb.conf.d/50-server.cnf << 'EOL' [server] -[mariadbd] +# [mysqld] is read by every MariaDB version; [mariadbd] only exists from +# 10.4.6, so focal's MariaDB 10.3 would silently ignore this whole block. +[mysqld] user = mysql pid-file = /run/mysqld/mysqld.pid basedir = /usr @@ -932,7 +956,7 @@ systemctl daemon-reload || handle_error "Failed to reload systemd daemon" # Enable and start all services (Consolidated Block) log_step "Enabling and restarting all services" -SERVICES=(nginx php${PHP_VERSION}-fpm supervisor openvpn@server freeradius valkey-server) +SERVICES=(nginx php${PHP_VERSION}-fpm supervisor openvpn@server freeradius "$VALKEY_SERVICE") for service in "${SERVICES[@]}"; do systemctl enable "$service" || handle_error "Failed to enable $service" systemctl restart "$service" || handle_error "Failed to restart $service" @@ -969,7 +993,7 @@ COMPLETED_STEPS+=("OpenVPN sandbox write access verified (ovpn_fix.sh)") # Final verification log_step "Verifying all services are running" -for service in nginx mariadb freeradius valkey-server php${PHP_VERSION}-fpm; do +for service in nginx mariadb freeradius "$VALKEY_SERVICE" php${PHP_VERSION}-fpm; do if ! systemctl is-active --quiet $service; then log_warning "$service is not running" systemctl status $service diff --git a/ubuntu_simplespot.sh b/ubuntu_simplespot.sh index 66d6796..d8689cc 100644 --- a/ubuntu_simplespot.sh +++ b/ubuntu_simplespot.sh @@ -121,6 +121,28 @@ echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/packages.networkradius.com.asc log_success "NetworkRADIUS repository configured for Ubuntu $UBUNTU_VERSION" COMPLETED_STEPS+=("NetworkRADIUS repository configured") +# Set Valkey package source. Ubuntu ships Valkey in its own archive only from +# noble (24.04, via noble-updates backport) onward; focal/jammy get Percona's +# builds instead, which use different package and service names. +log_step "Configuring Valkey package source" +if [[ "$UBUNTU_VERSION" == "focal" || "$UBUNTU_VERSION" == "jammy" ]]; then + # Remove conflicting redis packages + apt-get remove -y redis-tools redis-server || true + + # Install Percona release package and enable its Valkey repository + curl -fsSL "https://repo.percona.com/apt/percona-release_latest.${UBUNTU_VERSION}_all.deb" -o /tmp/percona-release_latest.deb || handle_error "Failed to download percona-release package" + dpkg -i /tmp/percona-release_latest.deb || handle_error "Failed to install percona-release package" + rm -f /tmp/percona-release_latest.deb + percona-release enable valkey experimental || handle_error "Failed to enable Percona Valkey repository" + + VALKEY_PACKAGES="valkey valkey-compat" + VALKEY_SERVICE="valkey" +else + VALKEY_PACKAGES="valkey-server valkey-tools valkey-redis-compat valkey-sentinel" + VALKEY_SERVICE="valkey-server" +fi +COMPLETED_STEPS+=("Valkey package source configured (service: ${VALKEY_SERVICE})") + # Set environment variable to avoid interactive prompts export DEBIAN_FRONTEND=noninteractive @@ -132,7 +154,7 @@ COMPLETED_STEPS+=("System packages updated") # Install required packages (Cleaned virtual PHP packages and freeradius-rest) log_step "Installing required packages" -INSTALL_PACKAGES="nginx-full python3-certbot-nginx php${PHP_VERSION}-fpm php${PHP_VERSION}-mysql php${PHP_VERSION}-cli php${PHP_VERSION}-curl php${PHP_VERSION}-zip php${PHP_VERSION}-common php${PHP_VERSION}-gd php${PHP_VERSION}-mbstring php${PHP_VERSION}-xml php${PHP_VERSION}-dev php${PHP_VERSION}-bcmath php${PHP_VERSION}-intl php${PHP_VERSION}-redis git unzip curl wget software-properties-common apt-transport-https ca-certificates gnupg lsb-release supervisor valkey-server valkey-tools valkey-redis-compat valkey-sentinel ufw openvpn easy-rsa freeradius freeradius-mysql freeradius-utils mariadb-server mariadb-client" +INSTALL_PACKAGES="nginx-full python3-certbot-nginx php${PHP_VERSION}-fpm php${PHP_VERSION}-mysql php${PHP_VERSION}-cli php${PHP_VERSION}-curl php${PHP_VERSION}-zip php${PHP_VERSION}-common php${PHP_VERSION}-gd php${PHP_VERSION}-mbstring php${PHP_VERSION}-xml php${PHP_VERSION}-dev php${PHP_VERSION}-bcmath php${PHP_VERSION}-intl php${PHP_VERSION}-redis git unzip curl wget software-properties-common apt-transport-https ca-certificates gnupg lsb-release supervisor ${VALKEY_PACKAGES} ufw openvpn easy-rsa freeradius freeradius-mysql freeradius-utils mariadb-server mariadb-client" if [ "$REINSTALL" = true ]; then log_info "Reinstalling packages (forcing configuration file replacement)" @@ -144,7 +166,7 @@ COMPLETED_STEPS+=("Required packages installed") # Configure Valkey service overrides log_step "Configuring Valkey service overrides" -VKEY_OVERRIDE_DIR="/etc/systemd/system/valkey.service.d" +VKEY_OVERRIDE_DIR="/etc/systemd/system/${VALKEY_SERVICE}.service.d" VKEY_OVERRIDE_FILE="${VKEY_OVERRIDE_DIR}/override.conf" mkdir -p "$VKEY_OVERRIDE_DIR" @@ -273,24 +295,24 @@ EOL # Restart Valkey to apply new configuration systemctl daemon-reexec || log_warning "daemon-reexec failed (non-critical)" systemctl daemon-reload || handle_error "Failed to reload systemd daemon" -systemctl restart valkey-server || handle_error "Failed to restart Valkey" -systemctl enable valkey-server || handle_error "Failed to enable Valkey" +systemctl restart "$VALKEY_SERVICE" || handle_error "Failed to restart Valkey" +systemctl enable "$VALKEY_SERVICE" || handle_error "Failed to enable Valkey" # Verify Valkey is running log_step "Verifying Valkey service status" -if systemctl is-active --quiet valkey-server; then +if systemctl is-active --quiet "$VALKEY_SERVICE"; then log_success "Valkey service is running" COMPLETED_STEPS+=("Valkey configured with ${MAX_MEMORY_MB}MB memory allocation") else log_warning "Valkey service is not running as expected. Checking status..." - systemctl status valkey-server --no-pager || true + systemctl status "$VALKEY_SERVICE" --no-pager || true log_info "Attempting to start Valkey service..." - if systemctl start valkey-server; then + if systemctl start "$VALKEY_SERVICE"; then log_success "Successfully started Valkey service" COMPLETED_STEPS+=("Valkey configured with ${MAX_MEMORY_MB}MB memory allocation") else - log_error "Failed to start Valkey service. Please check the logs with: journalctl -u valkey-server -n 50" + log_error "Failed to start Valkey service. Please check the logs with: journalctl -u ${VALKEY_SERVICE} -n 50" log_warning "Continuing installation despite Valkey service issue..." COMPLETED_STEPS+=("Valkey configuration completed but service failed to start") fi @@ -298,17 +320,17 @@ fi # Create Valkey debug script log_step "Creating Valkey debug script" -cat > /usr/local/bin/valkey-debug.sh << 'EOF' +cat > /usr/local/bin/valkey-debug.sh << EOF #!/bin/bash VALKEY_HOST="127.0.0.1" VALKEY_PORT="6379" echo "=== Valkey Status ===" -systemctl status valkey --no-pager -l +systemctl status ${VALKEY_SERVICE} --no-pager -l echo -e "\n=== Valkey Key Statistics ===" -echo "Total Keys in DB 0: $(valkey-cli -h $VALKEY_HOST -p $VALKEY_PORT dbsize)" +echo "Total Keys in DB 0: \$(valkey-cli -h \$VALKEY_HOST -p \$VALKEY_PORT dbsize)" EOF chmod +x /usr/local/bin/valkey-debug.sh || handle_error "Failed to make Valkey debug script executable" @@ -321,7 +343,7 @@ COMPLETED_STEPS+=("Monitoring cron job added") # Verify Valkey is working log_step "Verifying Valkey installation" -if ! systemctl is-active --quiet valkey; then handle_error "Valkey service is not running"; fi +if ! systemctl is-active --quiet "$VALKEY_SERVICE"; then handle_error "Valkey service is not running"; fi # Test Valkey connectivity and basic operations if [ "$(valkey-cli ping)" != "PONG" ]; then handle_error "Valkey is not responding to ping"; fi @@ -399,7 +421,9 @@ mkdir -p /etc/mysql/mariadb.conf.d/ || handle_error "Failed to create MariaDB co cat > /etc/mysql/mariadb.conf.d/50-server.cnf << 'EOL' [server] -[mariadbd] +# [mysqld] is read by every MariaDB version; [mariadbd] only exists from +# 10.4.6, so focal's MariaDB 10.3 would silently ignore this whole block. +[mysqld] user = mysql pid-file = /run/mysqld/mysqld.pid basedir = /usr @@ -932,7 +956,7 @@ systemctl daemon-reload || handle_error "Failed to reload systemd daemon" # Enable and start all services (Consolidated Block) log_step "Enabling and restarting all services" -SERVICES=(nginx php${PHP_VERSION}-fpm supervisor openvpn@server freeradius valkey-server) +SERVICES=(nginx php${PHP_VERSION}-fpm supervisor openvpn@server freeradius "$VALKEY_SERVICE") for service in "${SERVICES[@]}"; do systemctl enable "$service" || handle_error "Failed to enable $service" systemctl restart "$service" || handle_error "Failed to restart $service" @@ -969,7 +993,7 @@ COMPLETED_STEPS+=("OpenVPN sandbox write access verified (ovpn_fix.sh)") # Final verification log_step "Verifying all services are running" -for service in nginx mariadb freeradius valkey-server php${PHP_VERSION}-fpm; do +for service in nginx mariadb freeradius "$VALKEY_SERVICE" php${PHP_VERSION}-fpm; do if ! systemctl is-active --quiet $service; then log_warning "$service is not running" systemctl status $service From 6106401090659e1f0b71d6b51fd9152559a646a8 Mon Sep 17 00:00:00 2001 From: Barnabie Opiyo Date: Fri, 17 Jul 2026 17:32:56 +0300 Subject: [PATCH 22/45] Download helper scripts from mymanga/bash where they actually exist The installers curl universal.sh, db_cleanup.sh, ovpn_fix.sh and openvpn.sh from simpleisp/bash main, but only openvpn.sh exists there - the maintenance scripts were never pushed upstream, so the install aborted with "Failed to download universal.sh". Point all raw URLs at this fork, which has all four files. Co-Authored-By: Claude Fable 5 --- ubuntu_simpleisp.sh | 4 ++-- ubuntu_simplespot.sh | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ubuntu_simpleisp.sh b/ubuntu_simpleisp.sh index a38ac23..37df491 100644 --- a/ubuntu_simpleisp.sh +++ b/ubuntu_simpleisp.sh @@ -686,7 +686,7 @@ case $UBUNTU_VERSION in "focal"|"jammy"|"noble") echo "Installing OpenVPN for Ubuntu $UBUNTU_VERSION" export AUTO_INSTALL=y - curl -O https://raw.githubusercontent.com/simpleisp/bash/main/openvpn.sh || handle_error "Failed to download OpenVPN installer" + curl -O https://raw.githubusercontent.com/mymanga/bash/main/openvpn.sh || handle_error "Failed to download OpenVPN installer" chmod +x openvpn.sh || handle_error "Failed to make OpenVPN installer executable" ./openvpn.sh || handle_error "Failed to install OpenVPN" @@ -966,7 +966,7 @@ COMPLETED_STEPS+=("All services enabled and restarted") # Install maintenance scripts (autotune, DB cleanup, OpenVPN sandbox fix) log_step "Installing maintenance scripts to /var/www/html/sh" for s in universal.sh db_cleanup.sh ovpn_fix.sh; do - curl -fsSL "https://raw.githubusercontent.com/simpleisp/bash/main/${s}" -o "/var/www/html/sh/${s}" || handle_error "Failed to download ${s}" + curl -fsSL "https://raw.githubusercontent.com/mymanga/bash/main/${s}" -o "/var/www/html/sh/${s}" || handle_error "Failed to download ${s}" chmod +x "/var/www/html/sh/${s}" || handle_error "Failed to make ${s} executable" done COMPLETED_STEPS+=("Maintenance scripts installed to /var/www/html/sh") diff --git a/ubuntu_simplespot.sh b/ubuntu_simplespot.sh index d8689cc..ceb5125 100644 --- a/ubuntu_simplespot.sh +++ b/ubuntu_simplespot.sh @@ -686,7 +686,7 @@ case $UBUNTU_VERSION in "focal"|"jammy"|"noble") echo "Installing OpenVPN for Ubuntu $UBUNTU_VERSION" export AUTO_INSTALL=y - curl -O https://raw.githubusercontent.com/simpleisp/bash/main/openvpn.sh || handle_error "Failed to download OpenVPN installer" + curl -O https://raw.githubusercontent.com/mymanga/bash/main/openvpn.sh || handle_error "Failed to download OpenVPN installer" chmod +x openvpn.sh || handle_error "Failed to make OpenVPN installer executable" ./openvpn.sh || handle_error "Failed to install OpenVPN" @@ -966,7 +966,7 @@ COMPLETED_STEPS+=("All services enabled and restarted") # Install maintenance scripts (autotune, DB cleanup, OpenVPN sandbox fix) log_step "Installing maintenance scripts to /var/www/html/sh" for s in universal.sh db_cleanup.sh ovpn_fix.sh; do - curl -fsSL "https://raw.githubusercontent.com/simpleisp/bash/main/${s}" -o "/var/www/html/sh/${s}" || handle_error "Failed to download ${s}" + curl -fsSL "https://raw.githubusercontent.com/mymanga/bash/main/${s}" -o "/var/www/html/sh/${s}" || handle_error "Failed to download ${s}" chmod +x "/var/www/html/sh/${s}" || handle_error "Failed to make ${s} executable" done COMPLETED_STEPS+=("Maintenance scripts installed to /var/www/html/sh") From 655d59da54eb5dc4122e6b221181b6220faf2573 Mon Sep 17 00:00:00 2001 From: Barnabie Opiyo Date: Fri, 17 Jul 2026 17:48:19 +0300 Subject: [PATCH 23/45] Fix Valkey startup on Percona 8.0 packages (focal/jammy) Percona's valkey 8.0.0 deb ships /var/lib/valkey and /var/log/valkey owned by root and only chowns /etc/valkey/*.conf in its postinst, while valkey.service runs as user valkey. The package's own default config survives this (logfile "", dir ./), but our tuned config points logfile and the AOF dir at those root-owned paths, so the service exited at startup and "systemctl restart valkey" failed. The 7.2.x builds the existing fleet was installed with did not have this problem. Create and chown the data/log directories before restarting, and make the IPv6 bind optional (bind 0.0.0.0 -::0, same prefix Percona's default config uses) so hosts without IPv6 can still start Valkey. Co-Authored-By: Claude Fable 5 --- ubuntu_simpleisp.sh | 7 ++++++- ubuntu_simplespot.sh | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/ubuntu_simpleisp.sh b/ubuntu_simpleisp.sh index 37df491..845251f 100644 --- a/ubuntu_simpleisp.sh +++ b/ubuntu_simpleisp.sh @@ -204,10 +204,15 @@ log_info "Configuring Valkey with ${MAX_MEMORY_MB}MB memory allocation" # Create Valkey configuration directory if it doesn't exist mkdir -p /etc/valkey || handle_error "Failed to create Valkey configuration directory" +# Percona's valkey 8.x package ships /var/lib/valkey and /var/log/valkey owned +# by root while the service runs as user valkey; it must write both (AOF+log). +mkdir -p /var/lib/valkey /var/log/valkey || handle_error "Failed to create Valkey data/log directories" +chown -R valkey:valkey /var/lib/valkey /var/log/valkey || handle_error "Failed to set Valkey directory ownership" + # Configure Valkey with optimized settings for FreeRADIUS cat > /etc/valkey/valkey.conf << EOL # Valkey configuration for FreeRADIUS -bind 0.0.0.0 ::0 +bind 0.0.0.0 -::0 protected-mode yes port 6379 tcp-backlog 511 diff --git a/ubuntu_simplespot.sh b/ubuntu_simplespot.sh index ceb5125..9616e28 100644 --- a/ubuntu_simplespot.sh +++ b/ubuntu_simplespot.sh @@ -204,10 +204,15 @@ log_info "Configuring Valkey with ${MAX_MEMORY_MB}MB memory allocation" # Create Valkey configuration directory if it doesn't exist mkdir -p /etc/valkey || handle_error "Failed to create Valkey configuration directory" +# Percona's valkey 8.x package ships /var/lib/valkey and /var/log/valkey owned +# by root while the service runs as user valkey; it must write both (AOF+log). +mkdir -p /var/lib/valkey /var/log/valkey || handle_error "Failed to create Valkey data/log directories" +chown -R valkey:valkey /var/lib/valkey /var/log/valkey || handle_error "Failed to set Valkey directory ownership" + # Configure Valkey with optimized settings for FreeRADIUS cat > /etc/valkey/valkey.conf << EOL # Valkey configuration for FreeRADIUS -bind 0.0.0.0 ::0 +bind 0.0.0.0 -::0 protected-mode yes port 6379 tcp-backlog 511 From 9d75bdcb16eebea27eca2dcd11aa27208ec8df5f Mon Sep 17 00:00:00 2001 From: Barnabie Opiyo Date: Fri, 17 Jul 2026 17:51:50 +0300 Subject: [PATCH 24/45] Update clean_server.sh for the current installers - Stop valkey-sentinel (Percona postinst enables it) and openvpn@server - Strip installer-added root crontab entries (scheduler, valkey-debug, universal.sh, legacy jobs) - the installers append on every run, so leftovers duplicated on reinstall - Wipe valkey data/logs, valkey-debug.sh, and the valkey systemd override dirs for both unit names - Remove php-fpm/supervisor sandbox override drop-ins and daemon-reload - Remove appended www-data sudoers entries, with visudo validation and rollback so a bad edit can never brick sudo - Remove legacy update_memory_config.sh; glob ionCube ini cleanup across all PHP versions Co-Authored-By: Claude Fable 5 --- clean_server.sh | 51 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 10 deletions(-) diff --git a/clean_server.sh b/clean_server.sh index ec001d7..66815a2 100644 --- a/clean_server.sh +++ b/clean_server.sh @@ -43,9 +43,14 @@ fi echo "[$(date)] Detected PHP version: $PHP_VERSION" -# Stop services +# Stop services (valkey-server on noble, valkey + valkey-sentinel on focal/jammy Percona builds) log_step "Stopping services" -systemctl stop nginx freeradius mariadb valkey-server valkey php${PHP_VERSION}-fpm supervisor openvpn || echo "Could not stop all services" +systemctl stop nginx freeradius mariadb valkey-server valkey valkey-sentinel php${PHP_VERSION}-fpm supervisor openvpn openvpn@server || echo "Could not stop all services" + +# Remove installer-added cron entries from root's crontab. The installers +# append these on every run, so leftovers would duplicate on reinstall. +log_step "Removing installer cron entries" +crontab -l 2>/dev/null | grep -vE 'artisan schedule:run|valkey-debug\.sh|universal\.sh|db_cleanup\.sh|update_memory_config\.sh' | crontab - 2>/dev/null || echo "No crontab entries to clean" # Remove web files log_step "Removing web files" @@ -67,10 +72,35 @@ fi rm -rf /etc/openvpn 2>/dev/null rm -rf /etc/supervisor 2>/dev/null -# Remove valkey data -# log_step "Removing valkey data" -# rm -rf /var/lib/valkey/* 2>/dev/null -# rm -rf /var/lib/valkey/.* 2>/dev/null +# Remove valkey data, logs, debug script and systemd overrides +# (config in /etc/valkey is left in place - the installer rewrites it) +log_step "Removing valkey data and overrides" +rm -rf /var/lib/valkey/* 2>/dev/null +rm -f /var/log/valkey/*.log 2>/dev/null +rm -f /usr/local/bin/valkey-debug.sh 2>/dev/null +rm -rf /etc/systemd/system/valkey.service.d 2>/dev/null +rm -rf /etc/systemd/system/valkey-server.service.d 2>/dev/null + +# Remove installer systemd sandbox overrides (php-fpm / supervisor ReadWritePaths) +log_step "Removing systemd sandbox overrides" +rm -rf /etc/systemd/system/php*-fpm.service.d 2>/dev/null +rm -rf /etc/systemd/system/supervisor.service.d 2>/dev/null +systemctl daemon-reload 2>/dev/null + +# Remove installer-added www-data sudoers entries (appended on every install, +# so they would duplicate on reinstall). Validate before keeping the edit. +log_step "Removing www-data sudoers entries" +if grep -q '^www-data ALL=NOPASSWD:' /etc/sudoers; then + cp -a /etc/sudoers /etc/sudoers.cleanup.bak + sed -i '/^www-data ALL=NOPASSWD:/d' /etc/sudoers + if visudo -c >/dev/null 2>&1; then + rm -f /etc/sudoers.cleanup.bak + echo "[$(date)] www-data sudoers entries removed" + else + mv /etc/sudoers.cleanup.bak /etc/sudoers + echo "[$(date)] WARNING: sudoers validation failed, restored original" + fi +fi # Remove MySQL/MariaDB data and users log_step "Removing MySQL/MariaDB data and users" @@ -115,12 +145,13 @@ log_step "Removing application-specific files" # Preserve db.txt for credential reuse on reinstall # rm -f /root/db.txt 2>/dev/null rm -f /etc/cron.d/laravel-scheduler 2>/dev/null +rm -f /usr/local/bin/update_memory_config.sh 2>/dev/null -# Remove ionCube files +# Remove ionCube files (glob across all PHP versions in case detection missed one) log_step "Removing ionCube files" -rm -f /etc/php/${PHP_VERSION}/mods-available/ioncube.ini 2>/dev/null -rm -f /etc/php/${PHP_VERSION}/cli/conf.d/00-ioncube.ini 2>/dev/null -rm -f /etc/php/${PHP_VERSION}/fpm/conf.d/00-ioncube.ini 2>/dev/null +rm -f /etc/php/*/mods-available/ioncube.ini 2>/dev/null +rm -f /etc/php/*/cli/conf.d/00-ioncube.ini 2>/dev/null +rm -f /etc/php/*/fpm/conf.d/00-ioncube.ini 2>/dev/null # Preserve ionCube installation for reuse on reinstall # rm -rf /usr/local/ioncube 2>/dev/null find /usr/lib/php/ -name "*ioncube*" -delete 2>/dev/null From 0ce8164001d773f468aa6c93e7f0d05de71e1d4e Mon Sep 17 00:00:00 2001 From: Barnabie Opiyo Date: Fri, 17 Jul 2026 17:54:44 +0300 Subject: [PATCH 25/45] Document the PHP 7.4 pin for SimpleISP The panel has hardcoded code that breaks on PHP > 7.4, so PHP_VERSION stays 7.4 on every Ubuntu release (ondrej PPA ships 7.4 builds through noble). Comment the pin in the installer and note it in the README so it does not get bumped during future OS work. Co-Authored-By: Claude Fable 5 --- README.md | 2 +- ubuntu_simpleisp.sh | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 384c9c8..cde9094 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ sudo ./ubuntu_simpleisp.sh Each installer sets up the full stack: -- **nginx** + **PHP-FPM** serving the Laravel panel from `/var/www/html` (with Let's Encrypt via certbot) +- **nginx** + **PHP-FPM** serving the Laravel panel from `/var/www/html` (with Let's Encrypt via certbot). PHP is pinned per product — SimpleISP: **7.4** (the panel has code that breaks on newer PHP), SimpleSpot: **8.2** — and installs from the ondrej PPA on every supported Ubuntu release, so the pin holds on 24.04 too. - **MariaDB** (unix_socket root auth; credentials written to a file reported at the end of the install) - **FreeRADIUS 3.2** (NetworkRADIUS packages) with SQL accounting into the `radius` database - **Valkey** (Redis-compatible cache) with systemd hardening overrides — installed from Percona's repo on focal/jammy (service `valkey`), from the Ubuntu archive on noble (service `valkey-server`) diff --git a/ubuntu_simpleisp.sh b/ubuntu_simpleisp.sh index 845251f..6b20d9b 100644 --- a/ubuntu_simpleisp.sh +++ b/ubuntu_simpleisp.sh @@ -11,6 +11,9 @@ EMAIL_ADDRESS="simpluxsolutions@gmail.com" # Configuration variables - ONLY DIFFERENCES BETWEEN SCRIPTS GITHUB_REPO_URL="https://github.com/simpleisp/radius.git" +# Do NOT bump: the SimpleISP panel (Laravel 8) has hardcoded code that breaks +# on PHP > 7.4. The ondrej PPA provides php7.4 on every supported Ubuntu +# release including noble, so 7.4 stays pinned regardless of OS version. PHP_VERSION="7.4" # Logging functions From 65272eacf60cb9a1949a4d57dc18c461bb436714 Mon Sep 17 00:00:00 2001 From: Barnabie Opiyo Date: Fri, 17 Jul 2026 17:58:41 +0300 Subject: [PATCH 26/45] Make Valkey startable before any apt operation on focal/jammy Percona's valkey postinst runs "systemctl start valkey" directly with set -e, so every dpkg configure of the package - fresh install, apt upgrade, or recovery of a half-configured state left by a previously failed run - requires the service to actually start. A leftover tuned config pointing at root-owned /var/lib/valkey//var/log/valkey wedged dpkg: apt-get upgrade failed at Step 7 before the Step 10 ownership fix could ever run. Pre-create the valkey account and writable data/log dirs, and relax a leftover "bind 0.0.0.0 ::0" to the optional-IPv6 form, in the Valkey package-source step - before apt update/upgrade/install touch the package. This also self-heals servers currently stuck with valkey half-configured. Co-Authored-By: Claude Fable 5 --- ubuntu_simpleisp.sh | 12 ++++++++++++ ubuntu_simplespot.sh | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/ubuntu_simpleisp.sh b/ubuntu_simpleisp.sh index 6b20d9b..ed1246b 100644 --- a/ubuntu_simpleisp.sh +++ b/ubuntu_simpleisp.sh @@ -138,6 +138,18 @@ if [[ "$UBUNTU_VERSION" == "focal" || "$UBUNTU_VERSION" == "jammy" ]]; then rm -f /tmp/percona-release_latest.deb percona-release enable valkey experimental || handle_error "Failed to enable Percona Valkey repository" + # Percona's valkey postinst runs "systemctl start valkey" itself, so any + # dpkg configure (fresh install, upgrade, or recovery of a previously + # failed run) needs the service to be startable. Pre-create the valkey + # account and writable data/log dirs, and relax a leftover config that + # hard-requires IPv6, BEFORE any apt operation touches the package. + getent passwd valkey >/dev/null || useradd --system --user-group --home-dir /var/lib/valkey --no-create-home --shell /usr/sbin/nologin valkey || handle_error "Failed to create valkey user" + mkdir -p /var/lib/valkey /var/log/valkey || handle_error "Failed to create Valkey data/log directories" + chown -R valkey:valkey /var/lib/valkey /var/log/valkey || handle_error "Failed to set Valkey directory ownership" + if [ -f /etc/valkey/valkey.conf ]; then + sed -i 's/^bind 0\.0\.0\.0 ::0$/bind 0.0.0.0 -::0/' /etc/valkey/valkey.conf || true + fi + VALKEY_PACKAGES="valkey valkey-compat" VALKEY_SERVICE="valkey" else diff --git a/ubuntu_simplespot.sh b/ubuntu_simplespot.sh index 9616e28..dd5381c 100644 --- a/ubuntu_simplespot.sh +++ b/ubuntu_simplespot.sh @@ -135,6 +135,18 @@ if [[ "$UBUNTU_VERSION" == "focal" || "$UBUNTU_VERSION" == "jammy" ]]; then rm -f /tmp/percona-release_latest.deb percona-release enable valkey experimental || handle_error "Failed to enable Percona Valkey repository" + # Percona's valkey postinst runs "systemctl start valkey" itself, so any + # dpkg configure (fresh install, upgrade, or recovery of a previously + # failed run) needs the service to be startable. Pre-create the valkey + # account and writable data/log dirs, and relax a leftover config that + # hard-requires IPv6, BEFORE any apt operation touches the package. + getent passwd valkey >/dev/null || useradd --system --user-group --home-dir /var/lib/valkey --no-create-home --shell /usr/sbin/nologin valkey || handle_error "Failed to create valkey user" + mkdir -p /var/lib/valkey /var/log/valkey || handle_error "Failed to create Valkey data/log directories" + chown -R valkey:valkey /var/lib/valkey /var/log/valkey || handle_error "Failed to set Valkey directory ownership" + if [ -f /etc/valkey/valkey.conf ]; then + sed -i 's/^bind 0\.0\.0\.0 ::0$/bind 0.0.0.0 -::0/' /etc/valkey/valkey.conf || true + fi + VALKEY_PACKAGES="valkey valkey-compat" VALKEY_SERVICE="valkey" else From 403a5aec1159fe8cef50ccdc34456fbe8fa67c23 Mon Sep 17 00:00:00 2001 From: Barnabie Opiyo Date: Fri, 17 Jul 2026 18:06:20 +0300 Subject: [PATCH 27/45] Survive missing nginx/supervisor configs when reinstall marker was consumed The cleanup marker was deleted at the START of a run, so an install that failed later left subsequent retries in normal mode, where apt does not restore the conffiles clean_server.sh deleted. That surfaced as "mv: cannot stat /etc/nginx/sites-available/default" at the Nginx step, with the same trap waiting at the supervisor step (/etc/supervisor is wiped by cleanup). - Only back up nginx's default site if it exists (the script rewrites it immediately anyway) - Restore supervisor's package config via --force-confmiss if supervisord.conf is missing, and mkdir conf.d before writing into it - Delete the cleanup marker only after a fully successful install, so failed runs keep reinstall mode active for the next attempt Co-Authored-By: Claude Fable 5 --- ubuntu_simpleisp.sh | 21 ++++++++++++++++----- ubuntu_simplespot.sh | 21 ++++++++++++++++----- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/ubuntu_simpleisp.sh b/ubuntu_simpleisp.sh index ed1246b..d6e2109 100644 --- a/ubuntu_simpleisp.sh +++ b/ubuntu_simpleisp.sh @@ -57,10 +57,9 @@ if [ -f "$CLEANUP_MARKER" ]; then log_info "Detected previous cleanup ($(cat $CLEANUP_MARKER))" log_info "Forcing reinstallation of critical directories and files" REINSTALL=true - - # Remove the marker file after handling it - rm -f "$CLEANUP_MARKER" - log_success "Cleanup marker processed and removed" + # The marker is removed only at the end of a successful install, so a + # failed run keeps reinstall mode active for the next attempt. + log_success "Cleanup marker processed" fi # Ensure script runs as root @@ -540,7 +539,9 @@ COMPLETED_STEPS+=("Composer installed") # Configure Nginx log_step "Configuring Nginx" -mv /etc/nginx/sites-available/default /etc/nginx/sites-available/default.bak || handle_error "Failed to backup default Nginx site" +if [ -f /etc/nginx/sites-available/default ]; then + mv /etc/nginx/sites-available/default /etc/nginx/sites-available/default.bak || handle_error "Failed to backup default Nginx site" +fi touch /etc/nginx/sites-available/default || handle_error "Failed to create new Nginx site" ln -sf /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default || handle_error "Failed to symlink Nginx site" @@ -684,6 +685,12 @@ COMPLETED_STEPS+=("RADIUS database indexes optimized") # Configure Supervisor for queue worker (Pre-created log dir fix) log_step "Configuring Supervisor for queue worker" +# clean_server.sh wipes /etc/supervisor; restore the package config if a +# failed run already consumed the reinstall marker. +if [ ! -f /etc/supervisor/supervisord.conf ]; then + apt-get install --reinstall -y -o Dpkg::Options::="--force-confmiss" supervisor || handle_error "Failed to restore supervisor configuration" +fi +mkdir -p /etc/supervisor/conf.d || handle_error "Failed to create supervisor conf.d directory" mkdir -p /var/www/html/storage/logs || handle_error "Failed to create Laravel logs directory" cat > /etc/supervisor/conf.d/queue-worker.conf << "EOL" [program:queue-worker] @@ -1022,6 +1029,10 @@ for service in nginx mariadb freeradius "$VALKEY_SERVICE" php${PHP_VERSION}-fpm; fi done +# Install finished: clear the reinstall marker only now, so failed runs +# before this point keep reinstall mode for the next attempt. +rm -f "$CLEANUP_MARKER" + # Complete installation message log_success "Installation completed successfully!" echo "You can find your database credentials in $DB_CREDENTIALS_FILE" diff --git a/ubuntu_simplespot.sh b/ubuntu_simplespot.sh index dd5381c..cd9e3ed 100644 --- a/ubuntu_simplespot.sh +++ b/ubuntu_simplespot.sh @@ -54,10 +54,9 @@ if [ -f "$CLEANUP_MARKER" ]; then log_info "Detected previous cleanup ($(cat $CLEANUP_MARKER))" log_info "Forcing reinstallation of critical directories and files" REINSTALL=true - - # Remove the marker file after handling it - rm -f "$CLEANUP_MARKER" - log_success "Cleanup marker processed and removed" + # The marker is removed only at the end of a successful install, so a + # failed run keeps reinstall mode active for the next attempt. + log_success "Cleanup marker processed" fi # Ensure script runs as root @@ -537,7 +536,9 @@ COMPLETED_STEPS+=("Composer installed") # Configure Nginx log_step "Configuring Nginx" -mv /etc/nginx/sites-available/default /etc/nginx/sites-available/default.bak || handle_error "Failed to backup default Nginx site" +if [ -f /etc/nginx/sites-available/default ]; then + mv /etc/nginx/sites-available/default /etc/nginx/sites-available/default.bak || handle_error "Failed to backup default Nginx site" +fi touch /etc/nginx/sites-available/default || handle_error "Failed to create new Nginx site" ln -sf /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default || handle_error "Failed to symlink Nginx site" @@ -681,6 +682,12 @@ COMPLETED_STEPS+=("RADIUS database indexes optimized") # Configure Supervisor for queue worker (Pre-created log dir fix) log_step "Configuring Supervisor for queue worker" +# clean_server.sh wipes /etc/supervisor; restore the package config if a +# failed run already consumed the reinstall marker. +if [ ! -f /etc/supervisor/supervisord.conf ]; then + apt-get install --reinstall -y -o Dpkg::Options::="--force-confmiss" supervisor || handle_error "Failed to restore supervisor configuration" +fi +mkdir -p /etc/supervisor/conf.d || handle_error "Failed to create supervisor conf.d directory" mkdir -p /var/www/html/storage/logs || handle_error "Failed to create Laravel logs directory" cat > /etc/supervisor/conf.d/queue-worker.conf << "EOL" [program:queue-worker] @@ -1019,6 +1026,10 @@ for service in nginx mariadb freeradius "$VALKEY_SERVICE" php${PHP_VERSION}-fpm; fi done +# Install finished: clear the reinstall marker only now, so failed runs +# before this point keep reinstall mode for the next attempt. +rm -f "$CLEANUP_MARKER" + # Complete installation message log_success "Installation completed successfully!" echo "You can find your database credentials in $DB_CREDENTIALS_FILE" From b8eeeefe5e1ab017bd4216601b841475862f0b0b Mon Sep 17 00:00:00 2001 From: Barnabie Opiyo Date: Fri, 17 Jul 2026 18:08:04 +0300 Subject: [PATCH 28/45] Make DB creation, cron installs and sudoers append idempotent on retries A retried run on a partially installed server previously failed or left duplicates: CREATE DATABASE/CREATE USER errored because the previous attempt already created them (credentials are reused from db.txt), each retry appended another copy of every cron entry, and the www-data sudoers block was appended on every run. - CREATE DATABASE/USER IF NOT EXISTS, plus ALTER USER to keep the password in sync with db.txt - Cron installs strip their own previous entries before appending - The sudoers block is only appended if not already present Co-Authored-By: Claude Fable 5 --- ubuntu_simpleisp.sh | 15 +++++++++------ ubuntu_simplespot.sh | 15 +++++++++------ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/ubuntu_simpleisp.sh b/ubuntu_simpleisp.sh index d6e2109..b55b698 100644 --- a/ubuntu_simpleisp.sh +++ b/ubuntu_simpleisp.sh @@ -357,7 +357,7 @@ COMPLETED_STEPS+=("Valkey monitoring configured") # Add monitoring cron job (Safe Append Fix) log_step "Adding monitoring cron job" -(crontab -l 2>/dev/null; echo "*/5 * * * * /usr/local/bin/valkey-debug.sh") | crontab - || handle_error "Failed to install monitoring cron job" +(crontab -l 2>/dev/null | grep -v 'valkey-debug\.sh'; echo "*/5 * * * * /usr/local/bin/valkey-debug.sh") | crontab - || handle_error "Failed to install monitoring cron job" COMPLETED_STEPS+=("Monitoring cron job added") # Verify Valkey is working @@ -520,8 +520,9 @@ COMPLETED_STEPS+=("MariaDB installation secured") # Create database and user log_step "Creating database and user" -mysql -e "CREATE DATABASE $MYSQL_DATABASE;" || handle_error "Failed to create database" -mysql -e "CREATE USER '$MYSQL_USER'@'%' IDENTIFIED BY '$MYSQL_PASSWORD';" || handle_error "Failed to create database user" +mysql -e "CREATE DATABASE IF NOT EXISTS $MYSQL_DATABASE;" || handle_error "Failed to create database" +mysql -e "CREATE USER IF NOT EXISTS '$MYSQL_USER'@'%' IDENTIFIED BY '$MYSQL_PASSWORD';" || handle_error "Failed to create database user" +mysql -e "ALTER USER '$MYSQL_USER'@'%' IDENTIFIED BY '$MYSQL_PASSWORD';" || handle_error "Failed to set database user password" mysql -e "GRANT ALL PRIVILEGES ON $MYSQL_DATABASE.* TO '$MYSQL_USER'@'%';" || handle_error "Failed to grant database privileges" mysql -e "FLUSH PRIVILEGES;" || handle_error "Failed to flush MariaDB privileges" COMPLETED_STEPS+=("Database and user created with full access") @@ -745,11 +746,13 @@ COMPLETED_STEPS+=("Systemd sandbox overrides configured") # Install Laravel cron (Safe Append Fix) log_step "Installing cron" -(crontab -l 2>/dev/null; echo "* * * * * php /var/www/html/artisan schedule:run >> /dev/null 2>&1") | crontab - || handle_error "Failed to install Laravel cron job" +(crontab -l 2>/dev/null | grep -v 'artisan schedule:run'; echo "* * * * * php /var/www/html/artisan schedule:run >> /dev/null 2>&1") | crontab - || handle_error "Failed to install Laravel cron job" COMPLETED_STEPS+=("Cron job installed") # Update sudoers for www-data user (Updated openvpn explicit target) log_step "Updating sudoers for www-data user" +# Append only once - retries and reinstalls must not duplicate the block +if ! grep -qF 'www-data ALL=NOPASSWD: /var/www/html/sh/restart-services.sh' /etc/sudoers; then cat >> /etc/sudoers << 'EOL' www-data ALL=NOPASSWD: /bin/systemctl start openvpn@server www-data ALL=NOPASSWD: /bin/systemctl stop openvpn@server @@ -776,6 +779,7 @@ www-data ALL=NOPASSWD: /bin/systemctl status ssh www-data ALL=NOPASSWD: /var/www/html/sh/set_permissions.sh www-data ALL=NOPASSWD: /var/www/html/sh/restart-services.sh EOL +fi COMPLETED_STEPS+=("Sudoers updated for www-data user") # Open Firewall Ports and enable ufw @@ -1002,8 +1006,7 @@ COMPLETED_STEPS+=("Maintenance scripts installed to /var/www/html/sh") # reboot (delayed so MariaDB/Valkey/PHP-FPM are up before live tuning starts). # db_cleanup.sh is intentionally NOT scheduled - run it manually when needed. log_step "Scheduling maintenance cron jobs" -(crontab -l 2>/dev/null; echo "0 3 * * * /var/www/html/sh/universal.sh") | crontab - || handle_error "Failed to add universal.sh daily cron job" -(crontab -l 2>/dev/null; echo "@reboot sleep 120; /var/www/html/sh/universal.sh") | crontab - || handle_error "Failed to add universal.sh reboot cron job" +(crontab -l 2>/dev/null | grep -v 'universal\.sh'; echo "0 3 * * * /var/www/html/sh/universal.sh"; echo "@reboot sleep 120; /var/www/html/sh/universal.sh") | crontab - || handle_error "Failed to schedule universal.sh cron jobs" COMPLETED_STEPS+=("Maintenance cron jobs scheduled (universal.sh 3AM + reboot)") # Run the autotune once to apply the initial configuration diff --git a/ubuntu_simplespot.sh b/ubuntu_simplespot.sh index cd9e3ed..7f984e1 100644 --- a/ubuntu_simplespot.sh +++ b/ubuntu_simplespot.sh @@ -354,7 +354,7 @@ COMPLETED_STEPS+=("Valkey monitoring configured") # Add monitoring cron job (Safe Append Fix) log_step "Adding monitoring cron job" -(crontab -l 2>/dev/null; echo "*/5 * * * * /usr/local/bin/valkey-debug.sh") | crontab - || handle_error "Failed to install monitoring cron job" +(crontab -l 2>/dev/null | grep -v 'valkey-debug\.sh'; echo "*/5 * * * * /usr/local/bin/valkey-debug.sh") | crontab - || handle_error "Failed to install monitoring cron job" COMPLETED_STEPS+=("Monitoring cron job added") # Verify Valkey is working @@ -517,8 +517,9 @@ COMPLETED_STEPS+=("MariaDB installation secured") # Create database and user log_step "Creating database and user" -mysql -e "CREATE DATABASE $MYSQL_DATABASE;" || handle_error "Failed to create database" -mysql -e "CREATE USER '$MYSQL_USER'@'%' IDENTIFIED BY '$MYSQL_PASSWORD';" || handle_error "Failed to create database user" +mysql -e "CREATE DATABASE IF NOT EXISTS $MYSQL_DATABASE;" || handle_error "Failed to create database" +mysql -e "CREATE USER IF NOT EXISTS '$MYSQL_USER'@'%' IDENTIFIED BY '$MYSQL_PASSWORD';" || handle_error "Failed to create database user" +mysql -e "ALTER USER '$MYSQL_USER'@'%' IDENTIFIED BY '$MYSQL_PASSWORD';" || handle_error "Failed to set database user password" mysql -e "GRANT ALL PRIVILEGES ON $MYSQL_DATABASE.* TO '$MYSQL_USER'@'%';" || handle_error "Failed to grant database privileges" mysql -e "FLUSH PRIVILEGES;" || handle_error "Failed to flush MariaDB privileges" COMPLETED_STEPS+=("Database and user created with full access") @@ -742,11 +743,13 @@ COMPLETED_STEPS+=("Systemd sandbox overrides configured") # Install Laravel cron (Safe Append Fix) log_step "Installing cron" -(crontab -l 2>/dev/null; echo "* * * * * php /var/www/html/artisan schedule:run >> /dev/null 2>&1") | crontab - || handle_error "Failed to install Laravel cron job" +(crontab -l 2>/dev/null | grep -v 'artisan schedule:run'; echo "* * * * * php /var/www/html/artisan schedule:run >> /dev/null 2>&1") | crontab - || handle_error "Failed to install Laravel cron job" COMPLETED_STEPS+=("Cron job installed") # Update sudoers for www-data user (Updated openvpn explicit target) log_step "Updating sudoers for www-data user" +# Append only once - retries and reinstalls must not duplicate the block +if ! grep -qF 'www-data ALL=NOPASSWD: /var/www/html/sh/restart-services.sh' /etc/sudoers; then cat >> /etc/sudoers << 'EOL' www-data ALL=NOPASSWD: /bin/systemctl start openvpn@server www-data ALL=NOPASSWD: /bin/systemctl stop openvpn@server @@ -773,6 +776,7 @@ www-data ALL=NOPASSWD: /bin/systemctl status ssh www-data ALL=NOPASSWD: /var/www/html/sh/set_permissions.sh www-data ALL=NOPASSWD: /var/www/html/sh/restart-services.sh EOL +fi COMPLETED_STEPS+=("Sudoers updated for www-data user") # Open Firewall Ports and enable ufw @@ -999,8 +1003,7 @@ COMPLETED_STEPS+=("Maintenance scripts installed to /var/www/html/sh") # reboot (delayed so MariaDB/Valkey/PHP-FPM are up before live tuning starts). # db_cleanup.sh is intentionally NOT scheduled - run it manually when needed. log_step "Scheduling maintenance cron jobs" -(crontab -l 2>/dev/null; echo "0 3 * * * /var/www/html/sh/universal.sh") | crontab - || handle_error "Failed to add universal.sh daily cron job" -(crontab -l 2>/dev/null; echo "@reboot sleep 120; /var/www/html/sh/universal.sh") | crontab - || handle_error "Failed to add universal.sh reboot cron job" +(crontab -l 2>/dev/null | grep -v 'universal\.sh'; echo "0 3 * * * /var/www/html/sh/universal.sh"; echo "@reboot sleep 120; /var/www/html/sh/universal.sh") | crontab - || handle_error "Failed to schedule universal.sh cron jobs" COMPLETED_STEPS+=("Maintenance cron jobs scheduled (universal.sh 3AM + reboot)") # Run the autotune once to apply the initial configuration From e4580466a40b0259c5ccb72c906a4161733ae8f0 Mon Sep 17 00:00:00 2001 From: Barnabie Opiyo Date: Fri, 17 Jul 2026 18:15:49 +0300 Subject: [PATCH 29/45] Clone the panel repo's default branch instead of hardcoded master simpleisp/radius uses main as its default branch, so "git clone -b master" failed with "Remote branch master not found". Clone without -b so the repo's default branch is used for both products. Co-Authored-By: Claude Fable 5 --- ubuntu_simpleisp.sh | 2 +- ubuntu_simplespot.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ubuntu_simpleisp.sh b/ubuntu_simpleisp.sh index b55b698..89298d0 100644 --- a/ubuntu_simpleisp.sh +++ b/ubuntu_simpleisp.sh @@ -601,7 +601,7 @@ if [ -d "$LOCAL_PATH" ]; then rm -rf "$LOCAL_PATH" || handle_error "Failed to remove existing web root" fi -git clone -b master "$REPO_URL" "$LOCAL_PATH" || handle_error "Failed to clone repository" +git clone "$REPO_URL" "$LOCAL_PATH" || handle_error "Failed to clone repository" cd "$LOCAL_PATH" || handle_error "Failed to change directory to web root" # Install Laravel dependencies diff --git a/ubuntu_simplespot.sh b/ubuntu_simplespot.sh index 7f984e1..2c247d6 100644 --- a/ubuntu_simplespot.sh +++ b/ubuntu_simplespot.sh @@ -598,7 +598,7 @@ if [ -d "$LOCAL_PATH" ]; then rm -rf "$LOCAL_PATH" || handle_error "Failed to remove existing web root" fi -git clone -b master "$REPO_URL" "$LOCAL_PATH" || handle_error "Failed to clone repository" +git clone "$REPO_URL" "$LOCAL_PATH" || handle_error "Failed to clone repository" cd "$LOCAL_PATH" || handle_error "Failed to change directory to web root" # Install Laravel dependencies From 386adccf6b93ebcca189f80e3e3ddf01db457d87 Mon Sep 17 00:00:00 2001 From: Barnabie Opiyo Date: Fri, 17 Jul 2026 18:20:47 +0300 Subject: [PATCH 30/45] Reuse existing SSL certificates non-interactively The reuse path ran certbot interactively and piped a single "1" answer, but certbot also asks about the HTTP->HTTPS redirect; with stdin exhausted it exited non-zero after reinstalling the cert, aborting the install with "Failed to reuse SSL certificate". Collapse both paths into one non-interactive call: --keep-until-expiring reinstalls an existing valid certificate into the rewritten nginx config without reissuing (no rate-limit usage) and requests a new certificate only when none exists; --redirect answers the redirect question. Co-Authored-By: Claude Fable 5 --- ubuntu_simpleisp.sh | 15 ++++++--------- ubuntu_simplespot.sh | 15 ++++++--------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/ubuntu_simpleisp.sh b/ubuntu_simpleisp.sh index 89298d0..e2388bd 100644 --- a/ubuntu_simpleisp.sh +++ b/ubuntu_simpleisp.sh @@ -576,15 +576,12 @@ COMPLETED_STEPS+=("Nginx configured") log_step "Configuring SSL with Certbot" echo "Configuring SSL certificate for $DOMAIN" -if [ -d "/etc/letsencrypt/live/$DOMAIN" ]; then - log_info "SSL certificate already exists for $DOMAIN, reusing existing certificate" - echo -e "1\n" | certbot --nginx -d "$DOMAIN" || handle_error "Failed to reuse SSL certificate" - COMPLETED_STEPS+=("SSL certificate reused (already exists)") -else - log_info "No existing SSL certificate found for $DOMAIN, requesting new certificate" - certbot --nginx -d "$DOMAIN" --agree-tos --email "$EMAIL_ADDRESS" --no-eff-email --non-interactive --redirect || handle_error "Failed to configure SSL with Certbot" - COMPLETED_STEPS+=("SSL configured with Certbot") -fi +# One non-interactive call covers both cases: with --keep-until-expiring +# certbot reinstalls an existing valid certificate into the fresh nginx +# config (no reissue, no rate-limit usage) and only requests a new one +# when none exists. +certbot --nginx -d "$DOMAIN" --agree-tos --email "$EMAIL_ADDRESS" --no-eff-email --non-interactive --redirect --keep-until-expiring || handle_error "Failed to configure SSL with Certbot" +COMPLETED_STEPS+=("SSL configured with Certbot") # Test the Nginx configuration (Graceful restart applied) log_step "Restarting Nginx" diff --git a/ubuntu_simplespot.sh b/ubuntu_simplespot.sh index 2c247d6..b5bce55 100644 --- a/ubuntu_simplespot.sh +++ b/ubuntu_simplespot.sh @@ -573,15 +573,12 @@ COMPLETED_STEPS+=("Nginx configured") log_step "Configuring SSL with Certbot" echo "Configuring SSL certificate for $DOMAIN" -if [ -d "/etc/letsencrypt/live/$DOMAIN" ]; then - log_info "SSL certificate already exists for $DOMAIN, reusing existing certificate" - echo -e "1\n" | certbot --nginx -d "$DOMAIN" || handle_error "Failed to reuse SSL certificate" - COMPLETED_STEPS+=("SSL certificate reused (already exists)") -else - log_info "No existing SSL certificate found for $DOMAIN, requesting new certificate" - certbot --nginx -d "$DOMAIN" --agree-tos --email "$EMAIL_ADDRESS" --no-eff-email --non-interactive --redirect || handle_error "Failed to configure SSL with Certbot" - COMPLETED_STEPS+=("SSL configured with Certbot") -fi +# One non-interactive call covers both cases: with --keep-until-expiring +# certbot reinstalls an existing valid certificate into the fresh nginx +# config (no reissue, no rate-limit usage) and only requests a new one +# when none exists. +certbot --nginx -d "$DOMAIN" --agree-tos --email "$EMAIL_ADDRESS" --no-eff-email --non-interactive --redirect --keep-until-expiring || handle_error "Failed to configure SSL with Certbot" +COMPLETED_STEPS+=("SSL configured with Certbot") # Test the Nginx configuration (Graceful restart applied) log_step "Restarting Nginx" From 5ff29a2c9814fa1ebcfb96d04bd68b6c27a98537 Mon Sep 17 00:00:00 2001 From: Barnabie Opiyo Date: Fri, 17 Jul 2026 19:03:25 +0300 Subject: [PATCH 31/45] Use Ubuntu archive FreeRADIUS on noble+, make buffered-sql accounting real Packaging: focal/jammy keep the NetworkRADIUS repo (their archives only have FreeRADIUS 3.0.x); noble and newer install FreeRADIUS 3.2 from the Ubuntu archive (3.2.5 in main with security updates) and remove any NetworkRADIUS repo/pin leftovers. The two packagings use different config roots (/etc/freeradius vs /etc/freeradius/3.0), so all FreeRADIUS paths now go through FREERADIUS_CONF_DIR. New Ubuntu releases take the archive path by default instead of hard-failing. Accounting: previously the default site wrote accounting directly to SQL while the enabled buffered-sql site sat idle (the detail writer was commented out, so it had nothing to read). Wire it up properly: the default site's accounting section writes to a single local detail file, and a deterministic buffered-sql virtual server (track = yes) replays it into SQL - accounting survives MariaDB stalls and restarts. The detail module is rewritten as a single-file writer with locking; auth paths (authorize/session/post-auth) stay direct SQL. Validate the final config with freeradius -XC before services restart. clean_server.sh handles both config roots and removes the buffered accounting queue; README documents the split. Co-Authored-By: Claude Fable 5 --- README.md | 2 +- clean_server.sh | 19 ++++--- ubuntu_simpleisp.sh | 121 ++++++++++++++++++++++++++++++------------- ubuntu_simplespot.sh | 121 ++++++++++++++++++++++++++++++------------- 4 files changed, 183 insertions(+), 80 deletions(-) diff --git a/README.md b/README.md index cde9094..0ba7cba 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Each installer sets up the full stack: - **nginx** + **PHP-FPM** serving the Laravel panel from `/var/www/html` (with Let's Encrypt via certbot). PHP is pinned per product — SimpleISP: **7.4** (the panel has code that breaks on newer PHP), SimpleSpot: **8.2** — and installs from the ondrej PPA on every supported Ubuntu release, so the pin holds on 24.04 too. - **MariaDB** (unix_socket root auth; credentials written to a file reported at the end of the install) -- **FreeRADIUS 3.2** (NetworkRADIUS packages) with SQL accounting into the `radius` database +- **FreeRADIUS 3.2** with buffered SQL accounting into the `radius` database — accounting goes to a local detail file and the `buffered-sql` virtual server replays it into SQL, so records survive DB stalls/restarts. Packages come from NetworkRADIUS on focal/jammy (config root `/etc/freeradius/`, their archives only have 3.0.x) and from the Ubuntu archive on noble+ (config root `/etc/freeradius/3.0/`, 3.2 in main with security updates). Existing noble servers installed with NetworkRADIUS packages keep them — apt won't downgrade; only fresh installs switch. - **Valkey** (Redis-compatible cache) with systemd hardening overrides — installed from Percona's repo on focal/jammy (service `valkey`), from the Ubuntu archive on noble (service `valkey-server`) - **OpenVPN** (via `openvpn.sh`) with systemd `ReadWritePaths` overrides so the panel can manage `/etc/openvpn` - **supervisor** for Laravel queue workers, UFW rules, cron jobs, and sudoers entries for `www-data` service control diff --git a/clean_server.sh b/clean_server.sh index 66815a2..9c3ddfc 100644 --- a/clean_server.sh +++ b/clean_server.sh @@ -61,14 +61,19 @@ rm -rf /var/www/html/.* 2>/dev/null log_step "Removing configuration directories" rm -rf /etc/nginx/sites-available/default 2>/dev/null rm -rf /etc/nginx/sites-enabled/default 2>/dev/null -# Preserve FreeRADIUS base configuration, only remove application-specific configs -if [ -d "/etc/freeradius" ]; then - # Remove application-specific FreeRADIUS configurations but preserve base system configs - rm -f /etc/freeradius/mods-enabled/sql 2>/dev/null - rm -f /etc/freeradius/mods-enabled/rest 2>/dev/null +# Preserve FreeRADIUS base configuration, only remove application-specific +# configs. Handle both packagings: NetworkRADIUS (/etc/freeradius) and the +# Ubuntu archive (/etc/freeradius/3.0). The 3.0 dir nests inside the other, +# so guard on mods-enabled existing rather than the root. +for frdir in /etc/freeradius /etc/freeradius/3.0; do + [ -d "$frdir/mods-enabled" ] || continue + rm -f "$frdir/mods-enabled/sql" 2>/dev/null + rm -f "$frdir/mods-enabled/rest" 2>/dev/null # Remove any custom site configurations but preserve default - find /etc/freeradius/sites-enabled/ -name "*" ! -name "default" -delete 2>/dev/null -fi + find "$frdir/sites-enabled/" -name "*" ! -name "default" -delete 2>/dev/null +done +# Remove the buffered accounting queue file +rm -f /var/log/freeradius/radacct/detail* 2>/dev/null rm -rf /etc/openvpn 2>/dev/null rm -rf /etc/supervisor 2>/dev/null diff --git a/ubuntu_simpleisp.sh b/ubuntu_simpleisp.sh index e2388bd..d6fc5cd 100644 --- a/ubuntu_simpleisp.sh +++ b/ubuntu_simpleisp.sh @@ -102,26 +102,27 @@ else fi COMPLETED_STEPS+=("PHP repository added") -# Set NetworkRADIUS PGP public key -log_step "Configuring NetworkRADIUS repository" -install -d -o root -g root -m 0755 /etc/apt/keyrings || handle_error "Failed to create keyrings directory" -curl -s 'https://packages.networkradius.com/pgp/packages%40networkradius.com' | sudo tee /etc/apt/keyrings/packages.networkradius.com.asc > /dev/null || handle_error "Failed to download NetworkRADIUS PGP key" -COMPLETED_STEPS+=("NetworkRADIUS PGP key installed") - -# Add NetworkRADIUS APT preferences -printf 'Package: /freeradius/\nPin: origin "packages.networkradius.com"\nPin-Priority: 999\n' | sudo tee /etc/apt/preferences.d/networkradius > /dev/null || handle_error "Failed to set NetworkRADIUS preferences" - -# Add NetworkRADIUS repository based on Ubuntu version -case $UBUNTU_VERSION in - "noble") REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/noble noble main" ;; - "jammy") REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/jammy jammy main" ;; - "focal") REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/focal focal main" ;; - *) handle_error "Unsupported Ubuntu version: $UBUNTU_VERSION" ;; -esac - -echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/packages.networkradius.com.asc] $REPO_URL" | sudo tee /etc/apt/sources.list.d/networkradius.list > /dev/null || handle_error "Failed to add NetworkRADIUS repository" -log_success "NetworkRADIUS repository configured for Ubuntu $UBUNTU_VERSION" -COMPLETED_STEPS+=("NetworkRADIUS repository configured") +# Set FreeRADIUS package source. focal/jammy archives only carry FreeRADIUS +# 3.0.x, so they use the NetworkRADIUS vendor repo (config root +# /etc/freeradius). noble+ ships FreeRADIUS 3.2 in the Ubuntu archive with +# security updates; its packaging keeps the config under /etc/freeradius/3.0. +log_step "Configuring FreeRADIUS package source" +if [[ "$UBUNTU_VERSION" == "focal" || "$UBUNTU_VERSION" == "jammy" ]]; then + install -d -o root -g root -m 0755 /etc/apt/keyrings || handle_error "Failed to create keyrings directory" + curl -s 'https://packages.networkradius.com/pgp/packages%40networkradius.com' | sudo tee /etc/apt/keyrings/packages.networkradius.com.asc > /dev/null || handle_error "Failed to download NetworkRADIUS PGP key" + printf 'Package: /freeradius/\nPin: origin "packages.networkradius.com"\nPin-Priority: 999\n' | sudo tee /etc/apt/preferences.d/networkradius > /dev/null || handle_error "Failed to set NetworkRADIUS preferences" + REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/${UBUNTU_VERSION} ${UBUNTU_VERSION} main" + echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/packages.networkradius.com.asc] $REPO_URL" | sudo tee /etc/apt/sources.list.d/networkradius.list > /dev/null || handle_error "Failed to add NetworkRADIUS repository" + FREERADIUS_CONF_DIR="/etc/freeradius" + log_success "NetworkRADIUS repository configured for Ubuntu $UBUNTU_VERSION" +else + # Ubuntu archive packages; remove NetworkRADIUS leftovers so reruns and + # migrated servers actually install from the archive. + rm -f /etc/apt/sources.list.d/networkradius.list /etc/apt/preferences.d/networkradius + FREERADIUS_CONF_DIR="/etc/freeradius/3.0" + log_success "Using Ubuntu archive FreeRADIUS packages" +fi +COMPLETED_STEPS+=("FreeRADIUS package source configured (config root: ${FREERADIUS_CONF_DIR})") # Set Valkey package source. Ubuntu ships Valkey in its own archive only from # noble (24.04, via noble-updates backport) onward; focal/jammy get Percona's @@ -794,7 +795,7 @@ COMPLETED_STEPS+=("Firewall ports opened and ufw enabled") # Test FreeRADIUS configuration log_step "Checking FreeRADIUS files" -if [ ! -f "/etc/freeradius/radiusd.conf" ]; then +if [ ! -f "${FREERADIUS_CONF_DIR}/radiusd.conf" ]; then log_info "FreeRADIUS configuration files missing, reinstalling and reconfiguring FreeRADIUS package" apt-get purge -y freeradius freeradius-common freeradius-config 2>/dev/null || echo "FreeRADIUS not installed" apt-get autoremove -y 2>/dev/null @@ -802,10 +803,10 @@ if [ ! -f "/etc/freeradius/radiusd.conf" ]; then dpkg-reconfigure -f noninteractive freeradius-config 2>/dev/null || echo "Reconfigure not needed" fi -if [ ! -f "/etc/freeradius/radiusd.conf" ]; then +if [ ! -f "${FREERADIUS_CONF_DIR}/radiusd.conf" ]; then log_info "Creating minimal radiusd.conf configuration" - mkdir -p /etc/freeradius || handle_error "Failed to create freeradius config dir" - cat > /etc/freeradius/radiusd.conf << 'EOF' + mkdir -p "${FREERADIUS_CONF_DIR}" || handle_error "Failed to create freeradius config dir" + cat > "${FREERADIUS_CONF_DIR}/radiusd.conf" << 'EOF' prefix = /usr exec_prefix = ${prefix} sysconfdir = /etc @@ -876,30 +877,67 @@ $INCLUDE policy.d/ instantiate { } EOF - chmod 644 /etc/freeradius/radiusd.conf || handle_error "Failed to set radiusd.conf permissions" + # The heredoc is quoted (it contains FreeRADIUS's own ${var} syntax), so + # patch raddbdir to the packaging-specific config root afterwards. + sed -i "s|^raddbdir = .*|raddbdir = ${FREERADIUS_CONF_DIR}|" "${FREERADIUS_CONF_DIR}/radiusd.conf" || handle_error "Failed to set raddbdir" + chmod 644 "${FREERADIUS_CONF_DIR}/radiusd.conf" || handle_error "Failed to set radiusd.conf permissions" log_success "Created minimal radiusd.conf configuration" fi -if [ -f "/etc/freeradius/mods-available/sql" ]; then - ln -sf /etc/freeradius/mods-available/sql /etc/freeradius/mods-enabled/ || handle_error "Failed to re-enable SQL module" +if [ -f "${FREERADIUS_CONF_DIR}/mods-available/sql" ]; then + ln -sf "${FREERADIUS_CONF_DIR}/mods-available/sql" "${FREERADIUS_CONF_DIR}/mods-enabled/" || handle_error "Failed to re-enable SQL module" fi COMPLETED_STEPS+=("Completed checking FreeRADIUS files") -# Enable buffered-sql site +# Write and enable the buffered-sql site. Accounting packets are written to +# a local detail file by the default site (fast, survives DB stalls); this +# virtual server tails that file and replays the records into SQL. log_step "Enabling FreeRADIUS buffered-sql site" -mkdir -p /etc/freeradius/sites-enabled || handle_error "Failed to create FreeRADIUS sites-enabled directory" -ln -sf /etc/freeradius/sites-available/buffered-sql /etc/freeradius/sites-enabled/buffered-sql || handle_error "Failed to enable buffered-sql site" +mkdir -p "${FREERADIUS_CONF_DIR}/sites-available" "${FREERADIUS_CONF_DIR}/sites-enabled" || handle_error "Failed to create FreeRADIUS sites directories" +cat > "${FREERADIUS_CONF_DIR}/sites-available/buffered-sql" << 'EOF' +server buffered-sql { + listen { + type = detail + filename = "${radacctdir}/detail" + load_factor = 10 + track = yes + } + + preacct { + preprocess + } + + accounting { + sql + } +} +EOF +ln -sf "${FREERADIUS_CONF_DIR}/sites-available/buffered-sql" "${FREERADIUS_CONF_DIR}/sites-enabled/buffered-sql" || handle_error "Failed to enable buffered-sql site" COMPLETED_STEPS+=("FreeRADIUS buffered-sql site enabled") +# Configure the detail module as a single-file writer that buffered-sql can +# consume (the stock module writes per-NAS/per-day files the reader ignores). +log_step "Configuring FreeRADIUS detail module" +cat > "${FREERADIUS_CONF_DIR}/mods-available/detail" << 'EOF' +detail { + filename = ${radacctdir}/detail + header = "%t" + permissions = 0600 + locking = yes +} +EOF +ln -sf "${FREERADIUS_CONF_DIR}/mods-available/detail" "${FREERADIUS_CONF_DIR}/mods-enabled/detail" || handle_error "Failed to enable detail module" +COMPLETED_STEPS+=("FreeRADIUS detail module configured") + # Enable SQL module for FreeRADIUS log_step "Enabling SQL module" -mkdir -p /etc/freeradius/mods-enabled || handle_error "Failed to create FreeRADIUS mods-enabled directory" -ln -sf /etc/freeradius/mods-available/sql /etc/freeradius/mods-enabled/sql || handle_error "Failed to enable SQL module" +mkdir -p "${FREERADIUS_CONF_DIR}/mods-enabled" || handle_error "Failed to create FreeRADIUS mods-enabled directory" +ln -sf "${FREERADIUS_CONF_DIR}/mods-available/sql" "${FREERADIUS_CONF_DIR}/mods-enabled/sql" || handle_error "Failed to enable SQL module" COMPLETED_STEPS+=("FreeRADIUS SQL module enabled") # Write new FreeRADIUS SQL module log_step "Writing new FreeRADIUS SQL module" -SQL_FILE="/etc/freeradius/mods-available/sql" +SQL_FILE="${FREERADIUS_CONF_DIR}/mods-available/sql" cat > "$SQL_FILE" < /tmp/new_accounting_block accounting { -sql +detail exec attr_filter.accounting_response } @@ -978,6 +1017,16 @@ else fi COMPLETED_STEPS+=("FreeRADIUS default site configured") +# Validate the FreeRADIUS configuration now so wiring mistakes fail loudly +# here instead of at the service restart later. +log_step "Validating FreeRADIUS configuration" +RAD_BIN="$(command -v freeradius || command -v radiusd)" || handle_error "FreeRADIUS binary not found" +if ! "$RAD_BIN" -XC > /dev/null 2>&1; then + "$RAD_BIN" -XC 2>&1 | tail -n 20 + handle_error "FreeRADIUS configuration validation failed (freeradius -XC)" +fi +COMPLETED_STEPS+=("FreeRADIUS configuration validated") + # Apply Systemd Sandbox changes from the overrides systemctl daemon-reload || handle_error "Failed to reload systemd daemon" diff --git a/ubuntu_simplespot.sh b/ubuntu_simplespot.sh index b5bce55..ade804f 100644 --- a/ubuntu_simplespot.sh +++ b/ubuntu_simplespot.sh @@ -99,26 +99,27 @@ else fi COMPLETED_STEPS+=("PHP repository added") -# Set NetworkRADIUS PGP public key -log_step "Configuring NetworkRADIUS repository" -install -d -o root -g root -m 0755 /etc/apt/keyrings || handle_error "Failed to create keyrings directory" -curl -s 'https://packages.networkradius.com/pgp/packages%40networkradius.com' | sudo tee /etc/apt/keyrings/packages.networkradius.com.asc > /dev/null || handle_error "Failed to download NetworkRADIUS PGP key" -COMPLETED_STEPS+=("NetworkRADIUS PGP key installed") - -# Add NetworkRADIUS APT preferences -printf 'Package: /freeradius/\nPin: origin "packages.networkradius.com"\nPin-Priority: 999\n' | sudo tee /etc/apt/preferences.d/networkradius > /dev/null || handle_error "Failed to set NetworkRADIUS preferences" - -# Add NetworkRADIUS repository based on Ubuntu version -case $UBUNTU_VERSION in - "noble") REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/noble noble main" ;; - "jammy") REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/jammy jammy main" ;; - "focal") REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/focal focal main" ;; - *) handle_error "Unsupported Ubuntu version: $UBUNTU_VERSION" ;; -esac - -echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/packages.networkradius.com.asc] $REPO_URL" | sudo tee /etc/apt/sources.list.d/networkradius.list > /dev/null || handle_error "Failed to add NetworkRADIUS repository" -log_success "NetworkRADIUS repository configured for Ubuntu $UBUNTU_VERSION" -COMPLETED_STEPS+=("NetworkRADIUS repository configured") +# Set FreeRADIUS package source. focal/jammy archives only carry FreeRADIUS +# 3.0.x, so they use the NetworkRADIUS vendor repo (config root +# /etc/freeradius). noble+ ships FreeRADIUS 3.2 in the Ubuntu archive with +# security updates; its packaging keeps the config under /etc/freeradius/3.0. +log_step "Configuring FreeRADIUS package source" +if [[ "$UBUNTU_VERSION" == "focal" || "$UBUNTU_VERSION" == "jammy" ]]; then + install -d -o root -g root -m 0755 /etc/apt/keyrings || handle_error "Failed to create keyrings directory" + curl -s 'https://packages.networkradius.com/pgp/packages%40networkradius.com' | sudo tee /etc/apt/keyrings/packages.networkradius.com.asc > /dev/null || handle_error "Failed to download NetworkRADIUS PGP key" + printf 'Package: /freeradius/\nPin: origin "packages.networkradius.com"\nPin-Priority: 999\n' | sudo tee /etc/apt/preferences.d/networkradius > /dev/null || handle_error "Failed to set NetworkRADIUS preferences" + REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/${UBUNTU_VERSION} ${UBUNTU_VERSION} main" + echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/packages.networkradius.com.asc] $REPO_URL" | sudo tee /etc/apt/sources.list.d/networkradius.list > /dev/null || handle_error "Failed to add NetworkRADIUS repository" + FREERADIUS_CONF_DIR="/etc/freeradius" + log_success "NetworkRADIUS repository configured for Ubuntu $UBUNTU_VERSION" +else + # Ubuntu archive packages; remove NetworkRADIUS leftovers so reruns and + # migrated servers actually install from the archive. + rm -f /etc/apt/sources.list.d/networkradius.list /etc/apt/preferences.d/networkradius + FREERADIUS_CONF_DIR="/etc/freeradius/3.0" + log_success "Using Ubuntu archive FreeRADIUS packages" +fi +COMPLETED_STEPS+=("FreeRADIUS package source configured (config root: ${FREERADIUS_CONF_DIR})") # Set Valkey package source. Ubuntu ships Valkey in its own archive only from # noble (24.04, via noble-updates backport) onward; focal/jammy get Percona's @@ -791,7 +792,7 @@ COMPLETED_STEPS+=("Firewall ports opened and ufw enabled") # Test FreeRADIUS configuration log_step "Checking FreeRADIUS files" -if [ ! -f "/etc/freeradius/radiusd.conf" ]; then +if [ ! -f "${FREERADIUS_CONF_DIR}/radiusd.conf" ]; then log_info "FreeRADIUS configuration files missing, reinstalling and reconfiguring FreeRADIUS package" apt-get purge -y freeradius freeradius-common freeradius-config 2>/dev/null || echo "FreeRADIUS not installed" apt-get autoremove -y 2>/dev/null @@ -799,10 +800,10 @@ if [ ! -f "/etc/freeradius/radiusd.conf" ]; then dpkg-reconfigure -f noninteractive freeradius-config 2>/dev/null || echo "Reconfigure not needed" fi -if [ ! -f "/etc/freeradius/radiusd.conf" ]; then +if [ ! -f "${FREERADIUS_CONF_DIR}/radiusd.conf" ]; then log_info "Creating minimal radiusd.conf configuration" - mkdir -p /etc/freeradius || handle_error "Failed to create freeradius config dir" - cat > /etc/freeradius/radiusd.conf << 'EOF' + mkdir -p "${FREERADIUS_CONF_DIR}" || handle_error "Failed to create freeradius config dir" + cat > "${FREERADIUS_CONF_DIR}/radiusd.conf" << 'EOF' prefix = /usr exec_prefix = ${prefix} sysconfdir = /etc @@ -873,30 +874,67 @@ $INCLUDE policy.d/ instantiate { } EOF - chmod 644 /etc/freeradius/radiusd.conf || handle_error "Failed to set radiusd.conf permissions" + # The heredoc is quoted (it contains FreeRADIUS's own ${var} syntax), so + # patch raddbdir to the packaging-specific config root afterwards. + sed -i "s|^raddbdir = .*|raddbdir = ${FREERADIUS_CONF_DIR}|" "${FREERADIUS_CONF_DIR}/radiusd.conf" || handle_error "Failed to set raddbdir" + chmod 644 "${FREERADIUS_CONF_DIR}/radiusd.conf" || handle_error "Failed to set radiusd.conf permissions" log_success "Created minimal radiusd.conf configuration" fi -if [ -f "/etc/freeradius/mods-available/sql" ]; then - ln -sf /etc/freeradius/mods-available/sql /etc/freeradius/mods-enabled/ || handle_error "Failed to re-enable SQL module" +if [ -f "${FREERADIUS_CONF_DIR}/mods-available/sql" ]; then + ln -sf "${FREERADIUS_CONF_DIR}/mods-available/sql" "${FREERADIUS_CONF_DIR}/mods-enabled/" || handle_error "Failed to re-enable SQL module" fi COMPLETED_STEPS+=("Completed checking FreeRADIUS files") -# Enable buffered-sql site +# Write and enable the buffered-sql site. Accounting packets are written to +# a local detail file by the default site (fast, survives DB stalls); this +# virtual server tails that file and replays the records into SQL. log_step "Enabling FreeRADIUS buffered-sql site" -mkdir -p /etc/freeradius/sites-enabled || handle_error "Failed to create FreeRADIUS sites-enabled directory" -ln -sf /etc/freeradius/sites-available/buffered-sql /etc/freeradius/sites-enabled/buffered-sql || handle_error "Failed to enable buffered-sql site" +mkdir -p "${FREERADIUS_CONF_DIR}/sites-available" "${FREERADIUS_CONF_DIR}/sites-enabled" || handle_error "Failed to create FreeRADIUS sites directories" +cat > "${FREERADIUS_CONF_DIR}/sites-available/buffered-sql" << 'EOF' +server buffered-sql { + listen { + type = detail + filename = "${radacctdir}/detail" + load_factor = 10 + track = yes + } + + preacct { + preprocess + } + + accounting { + sql + } +} +EOF +ln -sf "${FREERADIUS_CONF_DIR}/sites-available/buffered-sql" "${FREERADIUS_CONF_DIR}/sites-enabled/buffered-sql" || handle_error "Failed to enable buffered-sql site" COMPLETED_STEPS+=("FreeRADIUS buffered-sql site enabled") +# Configure the detail module as a single-file writer that buffered-sql can +# consume (the stock module writes per-NAS/per-day files the reader ignores). +log_step "Configuring FreeRADIUS detail module" +cat > "${FREERADIUS_CONF_DIR}/mods-available/detail" << 'EOF' +detail { + filename = ${radacctdir}/detail + header = "%t" + permissions = 0600 + locking = yes +} +EOF +ln -sf "${FREERADIUS_CONF_DIR}/mods-available/detail" "${FREERADIUS_CONF_DIR}/mods-enabled/detail" || handle_error "Failed to enable detail module" +COMPLETED_STEPS+=("FreeRADIUS detail module configured") + # Enable SQL module for FreeRADIUS log_step "Enabling SQL module" -mkdir -p /etc/freeradius/mods-enabled || handle_error "Failed to create FreeRADIUS mods-enabled directory" -ln -sf /etc/freeradius/mods-available/sql /etc/freeradius/mods-enabled/sql || handle_error "Failed to enable SQL module" +mkdir -p "${FREERADIUS_CONF_DIR}/mods-enabled" || handle_error "Failed to create FreeRADIUS mods-enabled directory" +ln -sf "${FREERADIUS_CONF_DIR}/mods-available/sql" "${FREERADIUS_CONF_DIR}/mods-enabled/sql" || handle_error "Failed to enable SQL module" COMPLETED_STEPS+=("FreeRADIUS SQL module enabled") # Write new FreeRADIUS SQL module log_step "Writing new FreeRADIUS SQL module" -SQL_FILE="/etc/freeradius/mods-available/sql" +SQL_FILE="${FREERADIUS_CONF_DIR}/mods-available/sql" cat > "$SQL_FILE" < /tmp/new_accounting_block accounting { -sql +detail exec attr_filter.accounting_response } @@ -975,6 +1014,16 @@ else fi COMPLETED_STEPS+=("FreeRADIUS default site configured") +# Validate the FreeRADIUS configuration now so wiring mistakes fail loudly +# here instead of at the service restart later. +log_step "Validating FreeRADIUS configuration" +RAD_BIN="$(command -v freeradius || command -v radiusd)" || handle_error "FreeRADIUS binary not found" +if ! "$RAD_BIN" -XC > /dev/null 2>&1; then + "$RAD_BIN" -XC 2>&1 | tail -n 20 + handle_error "FreeRADIUS configuration validation failed (freeradius -XC)" +fi +COMPLETED_STEPS+=("FreeRADIUS configuration validated") + # Apply Systemd Sandbox changes from the overrides systemctl daemon-reload || handle_error "Failed to reload systemd daemon" From 6297997a3ed0cb3c5383533af87be86f622d5d19 Mon Sep 17 00:00:00 2001 From: Barnabie Opiyo Date: Fri, 17 Jul 2026 19:15:20 +0300 Subject: [PATCH 32/45] universal.sh: ensure buffered accounting wiring on existing servers (Step 6b) Rolls the installers' detail -> buffered-sql accounting out to the already-installed fleet on the next universal.sh run. Rewrites the detail module (single-file writer, locking), the buffered-sql virtual server (track = yes), and the default site's accounting section (detail/exec/attr_filter instead of direct sql), each with cmp-guarded writes, .bak.TIMESTAMP backups and prune_baks retention, so converted servers see no change and no restart on later runs. Validates the result with freeradius -XC before the end-of-run restart; on failure restores every touched file and removes freshly created enable symlinks. A successful change reuses the existing RADIUS_CHANGED restart-only-if-changed path. Full --dry-run support. Co-Authored-By: Claude Fable 5 --- universal.sh | 157 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/universal.sh b/universal.sh index e292af9..7611969 100644 --- a/universal.sh +++ b/universal.sh @@ -19,6 +19,11 @@ # v3 (2026-07-14): CAPACITY MODEL - one sizing block scales every component # from 2 vCPU / 2 GB to 16 vCPU / 16 GB (~5,000 concurrent users at top # tier). Copy this script unchanged to any size server; it self-sizes. +# +# v3.1 (2026-07-17): Step 6b ensures buffered accounting wiring (default +# site -> detail file -> buffered-sql -> SQL) so accounting survives DB +# stalls/restarts; validated with -XC, rolled back on failure, restart +# reuses the changed-only logic. Idempotent on already-wired servers. # --------------------------------------------------------------------------------- set -euo pipefail @@ -689,6 +694,158 @@ fi log "" +# ---------------------------------------------------------------------- +# Step 6b: Ensure buffered accounting wiring (detail -> buffered-sql). +# The default site writes accounting to a local detail file and the +# buffered-sql virtual server replays it into SQL, so records survive +# MariaDB stalls/restarts. Mirrors the installers; idempotent, so +# already-wired servers see no change and no restart. +# ---------------------------------------------------------------------- +log "========================" +log " Step 6b: Buffered accounting wiring (detail -> buffered-sql)" +log "------------------------" + +if [ -z "${RADIUS_CONF}" ]; then + log "[WARN] radiusd.conf not found; skipping buffered accounting wiring." +else + RAD_ROOT="$(dirname "${RADIUS_CONF}")" + BUFSQL_CHANGED=0 + BUFSQL_TOUCHED=() # files backed up this run, restored on failed validation + BUFSQL_NEW_LINKS=() # symlinks created this run, removed on failed validation + + # Desired detail module: a single-file writer buffered-sql can consume + # (stock writes per-NAS/per-day files the reader ignores). + DETAIL_MOD="${RAD_ROOT}/mods-available/detail" + DETAIL_TMP="${DETAIL_MOD}.new" + cat > "${DETAIL_TMP}" << 'EOF' +detail { + filename = ${radacctdir}/detail + header = "%t" + permissions = 0600 + locking = yes +} +EOF + + # Desired buffered-sql site: tail the detail file, replay into SQL. + BUFSQL_SITE="${RAD_ROOT}/sites-available/buffered-sql" + BUFSQL_TMP="${BUFSQL_SITE}.new" + cat > "${BUFSQL_TMP}" << 'EOF' +server buffered-sql { + listen { + type = detail + filename = "${radacctdir}/detail" + load_factor = 10 + track = yes + } + + preacct { + preprocess + } + + accounting { + sql + } +} +EOF + + # Desired default site: accounting section writes to detail only (SQL + # happens in buffered-sql). Regenerating is stable, so cmp below makes + # this a no-op on already-converted servers. + RAD_DEFAULT_SITE="${RAD_ROOT}/sites-enabled/default" + RAD_DEFAULT_TMP="" + if [ -f "${RAD_DEFAULT_SITE}" ]; then + RAD_DEFAULT_TMP="${RAD_DEFAULT_SITE}.new" + awk ' + BEGIN { skip = 0 } + /^accounting[ \t]*{/ { + print "accounting {" + print "detail" + print "exec" + print "attr_filter.accounting_response" + print "}" + skip = 1; next + } + /^[ \t]*}/ { if (skip) { skip = 0; next } } + !skip { print } + ' "${RAD_DEFAULT_SITE}" > "${RAD_DEFAULT_TMP}" || true + else + log "[WARN] ${RAD_DEFAULT_SITE} not found; accounting section left untouched" + fi + + bufsql_install_if_changed() { + # $1 = live file, $2 = desired temp file. Returns 0 if a change was + # made (or would be, under --dry-run), 1 if already up to date. + local live="$1" tmp="$2" + if [ -f "${live}" ] && cmp -s "${live}" "${tmp}"; then + rm -f "${tmp}" || true + return 1 + fi + if [ "$DRY_RUN" -eq 0 ]; then + if [ -f "${live}" ]; then + cp -a "${live}" "${live}.bak.${TIMESTAMP}" + prune_baks "${live}" + BUFSQL_TOUCHED+=("${live}") + fi + mv -f "${tmp}" "${live}" + log "[OK] updated ${live} (backup: ${live}.bak.${TIMESTAMP})" + else + rm -f "${tmp}" || true + log "[DRY] would update ${live}" + fi + return 0 + } + + if bufsql_install_if_changed "${DETAIL_MOD}" "${DETAIL_TMP}"; then BUFSQL_CHANGED=1; fi + if bufsql_install_if_changed "${BUFSQL_SITE}" "${BUFSQL_TMP}"; then BUFSQL_CHANGED=1; fi + if [ -n "${RAD_DEFAULT_TMP}" ]; then + if bufsql_install_if_changed "${RAD_DEFAULT_SITE}" "${RAD_DEFAULT_TMP}"; then BUFSQL_CHANGED=1; fi + fi + + # Enable the detail module and buffered-sql site if not already enabled. + for pair in "mods-available/detail:mods-enabled/detail" "sites-available/buffered-sql:sites-enabled/buffered-sql"; do + BUFSQL_SRC="${RAD_ROOT}/${pair%%:*}" + BUFSQL_DST="${RAD_ROOT}/${pair##*:}" + if [ ! -e "${BUFSQL_DST}" ]; then + if [ "$DRY_RUN" -eq 0 ]; then + ln -sf "${BUFSQL_SRC}" "${BUFSQL_DST}" + BUFSQL_NEW_LINKS+=("${BUFSQL_DST}") + log "[OK] enabled ${BUFSQL_DST}" + else + log "[DRY] would enable ${BUFSQL_DST}" + fi + BUFSQL_CHANGED=1 + fi + done + + if [ "${BUFSQL_CHANGED}" -eq 0 ]; then + log "[OK] buffered accounting already wired; no change, no restart" + elif [ "$DRY_RUN" -eq 0 ]; then + # Queue directory must exist before validation/first write. + mkdir -p /var/log/freeradius/radacct 2>/dev/null || true + if id freerad >/dev/null 2>&1; then + chown freerad:freerad /var/log/freeradius/radacct 2>/dev/null || true + fi + + # Validate BEFORE the restart at the end of the run; roll everything + # back on failure so a broken config never reaches the service. + RAD_BIN="$(command -v freeradius || command -v radiusd || true)" + if [ -n "${RAD_BIN}" ] && ! "${RAD_BIN}" -XC >/dev/null 2>&1; then + log "[ERROR] buffered accounting config FAILED validation (${RAD_BIN} -XC); rolling back" + for f in "${BUFSQL_TOUCHED[@]}"; do + if [ -f "${f}.bak.${TIMESTAMP}" ]; then cp -a "${f}.bak.${TIMESTAMP}" "${f}"; fi + done + for l in "${BUFSQL_NEW_LINKS[@]}"; do rm -f "${l}" || true; done + else + log "[OK] buffered accounting wiring applied and validated" + RADIUS_CHANGED=1 # reuse the existing end-of-run FreeRADIUS restart + fi + else + log "[DRY] would validate with -XC and restart FreeRADIUS if valid" + fi +fi + +log "" + # --------------------------------------------------------------------- # Step 7: PHP-FPM detection, tuning, slowlog setup, and IN-PLACE edits # --------------------------------------------------------------------- From f8751074ab5045edbfdc6b1848492908e37d1d24 Mon Sep 17 00:00:00 2001 From: Barnabie Opiyo Date: Fri, 17 Jul 2026 19:23:15 +0300 Subject: [PATCH 33/45] Keep sites-enabled/default a symlink; converge existing servers The default-site edit historically ran awk against sites-enabled/default and mv'd the result over it, replacing the packaged symlink with a regular file and leaving sites-available/default stale. Installers: recover an existing regular sites-enabled/default into sites-available/default when needed, apply the -sql/accounting edits to sites-available/default, and re-link sites-enabled/default to it. universal.sh Step 6b: generate the desired default site from the EFFECTIVE config (sites-enabled entry if present, else sites-available) with the same sed+awk the installers use, install it into sites-available/default, and normalize sites-enabled/default back to a symlink - backing up the old regular file and restoring it on failed validation (rollback now removes the path before copying the backup so it cannot write through a fresh symlink). Verified with 18 functional checks run under WSL for real symlink semantics: fleet-file conversion, source selection, backup, idempotent rerun, full rollback to the regular-file state, dry-run no-op, and pristine-tree (already-symlinked) conversion. Co-Authored-By: Claude Fable 5 --- ubuntu_simpleisp.sh | 16 ++++++++++--- ubuntu_simplespot.sh | 16 ++++++++++--- universal.sh | 56 ++++++++++++++++++++++++++++++++++---------- 3 files changed, 70 insertions(+), 18 deletions(-) diff --git a/ubuntu_simpleisp.sh b/ubuntu_simpleisp.sh index d6fc5cd..947f1b3 100644 --- a/ubuntu_simpleisp.sh +++ b/ubuntu_simpleisp.sh @@ -989,9 +989,18 @@ COMPLETED_STEPS+=("FreeRADIUS SQL module written with database credentials") # Configure FreeRADIUS default site log_step "Configuring FreeRADIUS default site" +DEFAULT_SITE_AVAIL="${FREERADIUS_CONF_DIR}/sites-available/default" DEFAULT_SITE="${FREERADIUS_CONF_DIR}/sites-enabled/default" -if [ -f "$DEFAULT_SITE" ]; then - sed -i 's/-sql/sql/g' "$DEFAULT_SITE" || handle_error "Failed to update -sql to sql" + +# Older installers edited sites-enabled/default in place, replacing the +# packaged symlink with a regular file; recover that content if needed, then +# always edit sites-available/default and re-link (the packaged layout). +if [ ! -f "$DEFAULT_SITE_AVAIL" ] && [ -f "$DEFAULT_SITE" ] && [ ! -L "$DEFAULT_SITE" ]; then + mv "$DEFAULT_SITE" "$DEFAULT_SITE_AVAIL" || handle_error "Failed to recover default site into sites-available" +fi + +if [ -f "$DEFAULT_SITE_AVAIL" ]; then + sed -i 's/-sql/sql/g' "$DEFAULT_SITE_AVAIL" || handle_error "Failed to update -sql to sql" log_step "Replacing accounting section in FreeRADIUS default site" # Accounting writes to the local detail file only; the buffered-sql # virtual server replays it into SQL (survives DB stalls/restarts). @@ -1011,7 +1020,8 @@ EOF while ((getline line < file) > 0) print line; close(file) } - ' "$DEFAULT_SITE" > /tmp/tmp_site && mv /tmp/tmp_site "$DEFAULT_SITE" + ' "$DEFAULT_SITE_AVAIL" > /tmp/tmp_site && mv /tmp/tmp_site "$DEFAULT_SITE_AVAIL" + ln -sf "$DEFAULT_SITE_AVAIL" "$DEFAULT_SITE" || handle_error "Failed to enable default site" else handle_error "Default site configuration file not found" fi diff --git a/ubuntu_simplespot.sh b/ubuntu_simplespot.sh index ade804f..a4b3556 100644 --- a/ubuntu_simplespot.sh +++ b/ubuntu_simplespot.sh @@ -986,9 +986,18 @@ COMPLETED_STEPS+=("FreeRADIUS SQL module written with database credentials") # Configure FreeRADIUS default site log_step "Configuring FreeRADIUS default site" +DEFAULT_SITE_AVAIL="${FREERADIUS_CONF_DIR}/sites-available/default" DEFAULT_SITE="${FREERADIUS_CONF_DIR}/sites-enabled/default" -if [ -f "$DEFAULT_SITE" ]; then - sed -i 's/-sql/sql/g' "$DEFAULT_SITE" || handle_error "Failed to update -sql to sql" + +# Older installers edited sites-enabled/default in place, replacing the +# packaged symlink with a regular file; recover that content if needed, then +# always edit sites-available/default and re-link (the packaged layout). +if [ ! -f "$DEFAULT_SITE_AVAIL" ] && [ -f "$DEFAULT_SITE" ] && [ ! -L "$DEFAULT_SITE" ]; then + mv "$DEFAULT_SITE" "$DEFAULT_SITE_AVAIL" || handle_error "Failed to recover default site into sites-available" +fi + +if [ -f "$DEFAULT_SITE_AVAIL" ]; then + sed -i 's/-sql/sql/g' "$DEFAULT_SITE_AVAIL" || handle_error "Failed to update -sql to sql" log_step "Replacing accounting section in FreeRADIUS default site" # Accounting writes to the local detail file only; the buffered-sql # virtual server replays it into SQL (survives DB stalls/restarts). @@ -1008,7 +1017,8 @@ EOF while ((getline line < file) > 0) print line; close(file) } - ' "$DEFAULT_SITE" > /tmp/tmp_site && mv /tmp/tmp_site "$DEFAULT_SITE" + ' "$DEFAULT_SITE_AVAIL" > /tmp/tmp_site && mv /tmp/tmp_site "$DEFAULT_SITE_AVAIL" + ln -sf "$DEFAULT_SITE_AVAIL" "$DEFAULT_SITE" || handle_error "Failed to enable default site" else handle_error "Default site configuration file not found" fi diff --git a/universal.sh b/universal.sh index 7611969..706abcc 100644 --- a/universal.sh +++ b/universal.sh @@ -748,14 +748,26 @@ server buffered-sql { } EOF - # Desired default site: accounting section writes to detail only (SQL - # happens in buffered-sql). Regenerating is stable, so cmp below makes - # this a no-op on already-converted servers. - RAD_DEFAULT_SITE="${RAD_ROOT}/sites-enabled/default" + # Desired default site, matching the installers exactly: -sql refs + # enabled and the accounting section writing to detail only (SQL happens + # in buffered-sql). Generated from the currently EFFECTIVE config - + # sites-enabled/default if present (older installers left an edited + # regular file there), else sites-available/default. Regeneration is + # stable, so cmp below makes this a no-op on already-converted servers. + RAD_DEFAULT_AVAIL="${RAD_ROOT}/sites-available/default" + RAD_DEFAULT_LINK="${RAD_ROOT}/sites-enabled/default" RAD_DEFAULT_TMP="" - if [ -f "${RAD_DEFAULT_SITE}" ]; then - RAD_DEFAULT_TMP="${RAD_DEFAULT_SITE}.new" - awk ' + if [ -f "${RAD_DEFAULT_LINK}" ]; then + RAD_DEFAULT_SRC="${RAD_DEFAULT_LINK}" + elif [ -f "${RAD_DEFAULT_AVAIL}" ]; then + RAD_DEFAULT_SRC="${RAD_DEFAULT_AVAIL}" + else + RAD_DEFAULT_SRC="" + log "[WARN] default site not found under ${RAD_ROOT}; accounting section left untouched" + fi + if [ -n "${RAD_DEFAULT_SRC}" ]; then + RAD_DEFAULT_TMP="${RAD_DEFAULT_AVAIL}.new" + sed 's/-sql/sql/g' "${RAD_DEFAULT_SRC}" | awk ' BEGIN { skip = 0 } /^accounting[ \t]*{/ { print "accounting {" @@ -767,9 +779,7 @@ EOF } /^[ \t]*}/ { if (skip) { skip = 0; next } } !skip { print } - ' "${RAD_DEFAULT_SITE}" > "${RAD_DEFAULT_TMP}" || true - else - log "[WARN] ${RAD_DEFAULT_SITE} not found; accounting section left untouched" + ' > "${RAD_DEFAULT_TMP}" || true fi bufsql_install_if_changed() { @@ -798,7 +808,27 @@ EOF if bufsql_install_if_changed "${DETAIL_MOD}" "${DETAIL_TMP}"; then BUFSQL_CHANGED=1; fi if bufsql_install_if_changed "${BUFSQL_SITE}" "${BUFSQL_TMP}"; then BUFSQL_CHANGED=1; fi if [ -n "${RAD_DEFAULT_TMP}" ]; then - if bufsql_install_if_changed "${RAD_DEFAULT_SITE}" "${RAD_DEFAULT_TMP}"; then BUFSQL_CHANGED=1; fi + if bufsql_install_if_changed "${RAD_DEFAULT_AVAIL}" "${RAD_DEFAULT_TMP}"; then BUFSQL_CHANGED=1; fi + # Normalize sites-enabled/default back to the packaged symlink layout: + # older installers replaced the symlink with an edited regular file. + # The content now lives (converted) in sites-available/default. + if [ ! -L "${RAD_DEFAULT_LINK}" ]; then + if [ "$DRY_RUN" -eq 0 ]; then + if [ -f "${RAD_DEFAULT_LINK}" ]; then + cp -a "${RAD_DEFAULT_LINK}" "${RAD_DEFAULT_LINK}.bak.${TIMESTAMP}" + prune_baks "${RAD_DEFAULT_LINK}" + BUFSQL_TOUCHED+=("${RAD_DEFAULT_LINK}") + rm -f "${RAD_DEFAULT_LINK}" + else + BUFSQL_NEW_LINKS+=("${RAD_DEFAULT_LINK}") + fi + ln -sf "${RAD_DEFAULT_AVAIL}" "${RAD_DEFAULT_LINK}" + log "[OK] normalized ${RAD_DEFAULT_LINK} to a symlink -> ${RAD_DEFAULT_AVAIL}" + else + log "[DRY] would normalize ${RAD_DEFAULT_LINK} to a symlink -> ${RAD_DEFAULT_AVAIL}" + fi + BUFSQL_CHANGED=1 + fi fi # Enable the detail module and buffered-sql site if not already enabled. @@ -832,7 +862,9 @@ EOF if [ -n "${RAD_BIN}" ] && ! "${RAD_BIN}" -XC >/dev/null 2>&1; then log "[ERROR] buffered accounting config FAILED validation (${RAD_BIN} -XC); rolling back" for f in "${BUFSQL_TOUCHED[@]}"; do - if [ -f "${f}.bak.${TIMESTAMP}" ]; then cp -a "${f}.bak.${TIMESTAMP}" "${f}"; fi + # rm first: if the live path became a symlink this run, cp -a onto + # it would write through the link instead of replacing it. + if [ -f "${f}.bak.${TIMESTAMP}" ]; then rm -f "${f}"; cp -a "${f}.bak.${TIMESTAMP}" "${f}"; fi done for l in "${BUFSQL_NEW_LINKS[@]}"; do rm -f "${l}" || true; done else From a1259541e11167603111f813118795b6e873464e Mon Sep 17 00:00:00 2001 From: Barnabie Opiyo Date: Fri, 17 Jul 2026 23:22:17 +0300 Subject: [PATCH 34/45] Grant www-data the sudo commands the panel actually runs The panel invokes systemctl by bare name, which sudo resolves via secure_path to /usr/bin/systemctl, and its OpenVPN status check queries the umbrella "openvpn" unit - neither matches the existing sudoers entries (/bin/systemctl + openvpn@server only), so on 24.04 the check was denied ("command not allowed") and the panel displayed OpenVPN as stopped while the service was running fine. Append a second guarded sudoers block with /usr/bin/systemctl and /usr/bin/supervisorctl variants of every existing entry plus both path forms for the "openvpn" umbrella unit. Own guard line so already- installed servers gain the block on rerun. Co-Authored-By: Claude Fable 5 --- ubuntu_simpleisp.sh | 40 ++++++++++++++++++++++++++++++++++++++++ ubuntu_simplespot.sh | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/ubuntu_simpleisp.sh b/ubuntu_simpleisp.sh index 947f1b3..745fd58 100644 --- a/ubuntu_simpleisp.sh +++ b/ubuntu_simpleisp.sh @@ -778,6 +778,46 @@ www-data ALL=NOPASSWD: /var/www/html/sh/set_permissions.sh www-data ALL=NOPASSWD: /var/www/html/sh/restart-services.sh EOL fi + +# The panel also invokes systemctl by bare name (sudo resolves that via +# secure_path to /usr/bin/systemctl) and checks the umbrella "openvpn" +# unit - neither matches the /bin/... openvpn@server entries above, which +# made the panel show OpenVPN as stopped. Separate guard so existing +# servers gain these lines on rerun. +if ! grep -qF '/usr/bin/systemctl status openvpn' /etc/sudoers; then +cat >> /etc/sudoers << 'EOL' +www-data ALL=NOPASSWD: /usr/bin/systemctl start openvpn@server +www-data ALL=NOPASSWD: /usr/bin/systemctl stop openvpn@server +www-data ALL=NOPASSWD: /usr/bin/systemctl restart openvpn@server +www-data ALL=NOPASSWD: /usr/bin/systemctl status openvpn@server +www-data ALL=NOPASSWD: /usr/bin/systemctl reload openvpn@server +www-data ALL=NOPASSWD: /usr/bin/systemctl enable openvpn@server +www-data ALL=NOPASSWD: /usr/bin/systemctl disable openvpn@server +www-data ALL=NOPASSWD: /bin/systemctl start openvpn +www-data ALL=NOPASSWD: /bin/systemctl stop openvpn +www-data ALL=NOPASSWD: /bin/systemctl restart openvpn +www-data ALL=NOPASSWD: /bin/systemctl status openvpn +www-data ALL=NOPASSWD: /usr/bin/systemctl start openvpn +www-data ALL=NOPASSWD: /usr/bin/systemctl stop openvpn +www-data ALL=NOPASSWD: /usr/bin/systemctl restart openvpn +www-data ALL=NOPASSWD: /usr/bin/systemctl status openvpn +www-data ALL=NOPASSWD: /usr/bin/systemctl start freeradius +www-data ALL=NOPASSWD: /usr/bin/systemctl stop freeradius +www-data ALL=NOPASSWD: /usr/bin/systemctl restart freeradius +www-data ALL=NOPASSWD: /usr/bin/systemctl status freeradius +www-data ALL=NOPASSWD: /usr/bin/systemctl reload freeradius +www-data ALL=NOPASSWD: /usr/bin/systemctl enable freeradius +www-data ALL=NOPASSWD: /usr/bin/systemctl disable freeradius +www-data ALL=NOPASSWD: /usr/bin/supervisorctl stop all +www-data ALL=NOPASSWD: /usr/bin/supervisorctl reread +www-data ALL=NOPASSWD: /usr/bin/supervisorctl update +www-data ALL=NOPASSWD: /usr/bin/supervisorctl start all +www-data ALL=NOPASSWD: /usr/bin/supervisorctl restart all +www-data ALL=NOPASSWD: /usr/bin/supervisorctl status +www-data ALL=NOPASSWD: /usr/bin/systemctl restart supervisor +www-data ALL=NOPASSWD: /usr/bin/systemctl status ssh +EOL +fi COMPLETED_STEPS+=("Sudoers updated for www-data user") # Open Firewall Ports and enable ufw diff --git a/ubuntu_simplespot.sh b/ubuntu_simplespot.sh index a4b3556..3f8f6ad 100644 --- a/ubuntu_simplespot.sh +++ b/ubuntu_simplespot.sh @@ -775,6 +775,46 @@ www-data ALL=NOPASSWD: /var/www/html/sh/set_permissions.sh www-data ALL=NOPASSWD: /var/www/html/sh/restart-services.sh EOL fi + +# The panel also invokes systemctl by bare name (sudo resolves that via +# secure_path to /usr/bin/systemctl) and checks the umbrella "openvpn" +# unit - neither matches the /bin/... openvpn@server entries above, which +# made the panel show OpenVPN as stopped. Separate guard so existing +# servers gain these lines on rerun. +if ! grep -qF '/usr/bin/systemctl status openvpn' /etc/sudoers; then +cat >> /etc/sudoers << 'EOL' +www-data ALL=NOPASSWD: /usr/bin/systemctl start openvpn@server +www-data ALL=NOPASSWD: /usr/bin/systemctl stop openvpn@server +www-data ALL=NOPASSWD: /usr/bin/systemctl restart openvpn@server +www-data ALL=NOPASSWD: /usr/bin/systemctl status openvpn@server +www-data ALL=NOPASSWD: /usr/bin/systemctl reload openvpn@server +www-data ALL=NOPASSWD: /usr/bin/systemctl enable openvpn@server +www-data ALL=NOPASSWD: /usr/bin/systemctl disable openvpn@server +www-data ALL=NOPASSWD: /bin/systemctl start openvpn +www-data ALL=NOPASSWD: /bin/systemctl stop openvpn +www-data ALL=NOPASSWD: /bin/systemctl restart openvpn +www-data ALL=NOPASSWD: /bin/systemctl status openvpn +www-data ALL=NOPASSWD: /usr/bin/systemctl start openvpn +www-data ALL=NOPASSWD: /usr/bin/systemctl stop openvpn +www-data ALL=NOPASSWD: /usr/bin/systemctl restart openvpn +www-data ALL=NOPASSWD: /usr/bin/systemctl status openvpn +www-data ALL=NOPASSWD: /usr/bin/systemctl start freeradius +www-data ALL=NOPASSWD: /usr/bin/systemctl stop freeradius +www-data ALL=NOPASSWD: /usr/bin/systemctl restart freeradius +www-data ALL=NOPASSWD: /usr/bin/systemctl status freeradius +www-data ALL=NOPASSWD: /usr/bin/systemctl reload freeradius +www-data ALL=NOPASSWD: /usr/bin/systemctl enable freeradius +www-data ALL=NOPASSWD: /usr/bin/systemctl disable freeradius +www-data ALL=NOPASSWD: /usr/bin/supervisorctl stop all +www-data ALL=NOPASSWD: /usr/bin/supervisorctl reread +www-data ALL=NOPASSWD: /usr/bin/supervisorctl update +www-data ALL=NOPASSWD: /usr/bin/supervisorctl start all +www-data ALL=NOPASSWD: /usr/bin/supervisorctl restart all +www-data ALL=NOPASSWD: /usr/bin/supervisorctl status +www-data ALL=NOPASSWD: /usr/bin/systemctl restart supervisor +www-data ALL=NOPASSWD: /usr/bin/systemctl status ssh +EOL +fi COMPLETED_STEPS+=("Sudoers updated for www-data user") # Open Firewall Ports and enable ufw From 5b180e365861f67afb9b1d4c4af55b73230078c0 Mon Sep 17 00:00:00 2001 From: Barnabie Opiyo Date: Fri, 17 Jul 2026 23:25:56 +0300 Subject: [PATCH 35/45] universal.sh: ensure panel sudoers entries on existing servers (Step 6c) Rolls the installers' second sudoers block out to already-installed servers: /usr/bin/systemctl and /usr/bin/supervisorctl variants of every www-data grant plus both path forms for the umbrella "openvpn" unit. Without these the panel's status checks (bare systemctl resolved via sudo secure_path, querying "openvpn") were denied and OpenVPN showed as stopped while running. Guarded so already-fixed servers see no change; backs up /etc/sudoers with the usual .bak.TIMESTAMP + prune_baks retention, validates with visudo -c and restores the backup on failure. Full --dry-run support. Co-Authored-By: Claude Fable 5 --- universal.sh | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/universal.sh b/universal.sh index 706abcc..65d6a75 100644 --- a/universal.sh +++ b/universal.sh @@ -24,6 +24,10 @@ # site -> detail file -> buffered-sql -> SQL) so accounting survives DB # stalls/restarts; validated with -XC, rolled back on failure, restart # reuses the changed-only logic. Idempotent on already-wired servers. +# +# v3.2 (2026-07-17): Step 6c appends the /usr/bin systemctl/supervisorctl +# sudoers entries the panel actually invokes (plus the umbrella +# "openvpn" unit); validated with visudo -c, restored on failure. # --------------------------------------------------------------------------------- set -euo pipefail @@ -878,6 +882,67 @@ fi log "" +# ---------------------------------------------------------------------- +# Step 6c: Ensure the www-data sudoers entries the panel actually uses. +# The panel invokes systemctl by bare name (sudo's secure_path resolves +# it to /usr/bin/systemctl) and checks the umbrella "openvpn" unit; older +# installs only granted /bin/systemctl + openvpn@server, so the panel's +# status checks were denied and OpenVPN showed as stopped while running. +# Mirrors the installers' second sudoers block; idempotent. +# ---------------------------------------------------------------------- +log "========================" +log " Step 6c: Panel sudoers entries (www-data)" +log "------------------------" + +if grep -qF '/usr/bin/systemctl status openvpn' /etc/sudoers; then + log "[OK] sudoers already has the /usr/bin entries; no change" +elif [ "$DRY_RUN" -eq 0 ]; then + cp -a /etc/sudoers "/etc/sudoers.bak.${TIMESTAMP}" + prune_baks /etc/sudoers + cat >> /etc/sudoers << 'EOF' +www-data ALL=NOPASSWD: /usr/bin/systemctl start openvpn@server +www-data ALL=NOPASSWD: /usr/bin/systemctl stop openvpn@server +www-data ALL=NOPASSWD: /usr/bin/systemctl restart openvpn@server +www-data ALL=NOPASSWD: /usr/bin/systemctl status openvpn@server +www-data ALL=NOPASSWD: /usr/bin/systemctl reload openvpn@server +www-data ALL=NOPASSWD: /usr/bin/systemctl enable openvpn@server +www-data ALL=NOPASSWD: /usr/bin/systemctl disable openvpn@server +www-data ALL=NOPASSWD: /bin/systemctl start openvpn +www-data ALL=NOPASSWD: /bin/systemctl stop openvpn +www-data ALL=NOPASSWD: /bin/systemctl restart openvpn +www-data ALL=NOPASSWD: /bin/systemctl status openvpn +www-data ALL=NOPASSWD: /usr/bin/systemctl start openvpn +www-data ALL=NOPASSWD: /usr/bin/systemctl stop openvpn +www-data ALL=NOPASSWD: /usr/bin/systemctl restart openvpn +www-data ALL=NOPASSWD: /usr/bin/systemctl status openvpn +www-data ALL=NOPASSWD: /usr/bin/systemctl start freeradius +www-data ALL=NOPASSWD: /usr/bin/systemctl stop freeradius +www-data ALL=NOPASSWD: /usr/bin/systemctl restart freeradius +www-data ALL=NOPASSWD: /usr/bin/systemctl status freeradius +www-data ALL=NOPASSWD: /usr/bin/systemctl reload freeradius +www-data ALL=NOPASSWD: /usr/bin/systemctl enable freeradius +www-data ALL=NOPASSWD: /usr/bin/systemctl disable freeradius +www-data ALL=NOPASSWD: /usr/bin/supervisorctl stop all +www-data ALL=NOPASSWD: /usr/bin/supervisorctl reread +www-data ALL=NOPASSWD: /usr/bin/supervisorctl update +www-data ALL=NOPASSWD: /usr/bin/supervisorctl start all +www-data ALL=NOPASSWD: /usr/bin/supervisorctl restart all +www-data ALL=NOPASSWD: /usr/bin/supervisorctl status +www-data ALL=NOPASSWD: /usr/bin/systemctl restart supervisor +www-data ALL=NOPASSWD: /usr/bin/systemctl status ssh +EOF + if visudo -c >/dev/null 2>&1; then + log "[OK] appended /usr/bin sudoers entries for www-data (visudo -c validated; backup: /etc/sudoers.bak.${TIMESTAMP})" + else + cp -a "/etc/sudoers.bak.${TIMESTAMP}" /etc/sudoers + log "[ERROR] sudoers FAILED visudo validation after append; restored backup" + fi +else + log "[DRY] would append /usr/bin systemctl/supervisorctl sudoers entries for www-data" +fi + +log "" + # --------------------------------------------------------------------- # Step 7: PHP-FPM detection, tuning, slowlog setup, and IN-PLACE edits # --------------------------------------------------------------------- From f37deaff9cec782604941dd1f1ec316f17eefd6f Mon Sep 17 00:00:00 2001 From: Barnabie Opiyo Date: Thu, 23 Jul 2026 00:59:50 +0300 Subject: [PATCH 36/45] removed all installers --- ubuntu_simpleisp_old.sh | 1406 ------------------------------------ ubuntu_simplespot_old.sh | 1451 -------------------------------------- 2 files changed, 2857 deletions(-) delete mode 100644 ubuntu_simpleisp_old.sh delete mode 100644 ubuntu_simplespot_old.sh diff --git a/ubuntu_simpleisp_old.sh b/ubuntu_simpleisp_old.sh deleted file mode 100644 index 91aa1d6..0000000 --- a/ubuntu_simpleisp_old.sh +++ /dev/null @@ -1,1406 +0,0 @@ -#!/bin/bash - -# Setup logging and error handling -INSTALL_LOG="/root/install.txt" -STEP_COUNT=0 -COMPLETED_STEPS=() - -# Configuration variables - ONLY DIFFERENCES BETWEEN SCRIPTS -GITHUB_REPO_URL="https://github.com/simpleisp/radius.git" -PHP_VERSION="7.4" - -# Logging functions -log_info() { - echo "ℹ️ INFO: $1" | tee -a "$INSTALL_LOG" -} - -log_success() { - echo "✅ SUCCESS: $1" | tee -a "$INSTALL_LOG" -} - -log_error() { - echo "❌ ERROR: $1" | tee -a "$INSTALL_LOG" -} - -log_step() { - STEP_COUNT=$((STEP_COUNT + 1)) - echo "👉 STEP $STEP_COUNT: $1" | tee -a "$INSTALL_LOG" -} - -handle_error() { - log_error "$1" - echo -e "\nCompleted steps before failure:" - printf '%s\n' "${COMPLETED_STEPS[@]}" - echo -e "\nCheck $INSTALL_LOG for more details" - exit 1 -} - -# Initialize log file -touch "$INSTALL_LOG" || { echo "Cannot create log file"; exit 1; } -echo "SimpleISP Installation Log - $(date '+%Y-%m-%d %H:%M:%S')" > "$INSTALL_LOG" -echo "----------------------------------------" >> "$INSTALL_LOG" - -# Configure system for Valkey (memory overcommit and other optimizations) -log_step "Configuring system for Valkey" - -# Enable memory overcommit -if ! grep -q "^vm.overcommit_memory" /etc/sysctl.conf; then - echo "vm.overcommit_memory = 1" | tee -a /etc/sysctl.conf - sysctl -p /etc/sysctl.conf - log_info "Enabled memory overcommit in sysctl" -else - log_info "Memory overcommit already configured in sysctl" -fi -COMPLETED_STEPS+=("System configured for Valkey") - -# Check for cleanup marker file -CLEANUP_MARKER="/root/.simpleisp_cleanup_done" -REINSTALL=false - -if [ -f "$CLEANUP_MARKER" ]; then - log_info "Detected previous cleanup ($(cat $CLEANUP_MARKER))" - log_info "Forcing reinstallation of critical directories and files" - REINSTALL=true - - # Remove the marker file after handling it - rm -f "$CLEANUP_MARKER" - log_success "Cleanup marker processed and removed" -fi - -# Ensure script runs as root -log_step "Checking root privileges" -if [ "$EUID" -ne 0 ]; then - handle_error "Please run as root" -fi -COMPLETED_STEPS+=("Root check passed") - -# Set environment variable to avoid interactive prompts -export DEBIAN_FRONTEND=noninteractive -# Get Ubuntu version -log_step "Detecting Ubuntu version" -UBUNTU_VERSION=$(lsb_release -cs) || handle_error "Failed to detect Ubuntu version" -log_info "Detected Ubuntu version: $UBUNTU_VERSION" -COMPLETED_STEPS+=("Ubuntu version detected: $UBUNTU_VERSION") - -# Set PHP Repo for Ubuntu 24.04 (Noble) -log_step "Adding PHP repository" -if [ "$UBUNTU_VERSION" = "noble" ]; then - # Set up PHP repository for Ubuntu 24.04 - gpgKey='B8DC7E53946656EFBCE4C1DD71DAEAAB4AD4CAB6' - gpgKeyPath='/etc/apt/keyrings/ondrej-ubuntu-php.gpg' - gpgURL="https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x${gpgKey}" - - # Create keyrings directory if it doesn't exist - install -d -m 0755 /etc/apt/keyrings || handle_error "Failed to create keyrings directory" - - # Download and set up GPG key - curl "${gpgURL}" | gpg --dearmor | tee ${gpgKeyPath} >/dev/null || handle_error "Failed to setup PHP GPG key" - gpg --dry-run --quiet --import --import-options import-show ${gpgKeyPath} - - # Create the sources file for PHP repository - cat > /etc/apt/sources.list.d/ondrej-ubuntu-php-noble.sources << "EOL" -Types: deb -URIs: https://ppa.launchpadcontent.net/ondrej/php/ubuntu/ -Suites: noble -Components: main -Signed-By: /etc/apt/keyrings/ondrej-ubuntu-php.gpg -EOL -else - # For other Ubuntu versions, use the traditional PPA method - LC_ALL=C.UTF-8 add-apt-repository ppa:ondrej/php -y || handle_error "Failed to add PHP repository" -fi -COMPLETED_STEPS+=("PHP repository added") - -# Set NetworkRADIUS PGP public key -log_step "Configuring NetworkRADIUS repository" -install -d -o root -g root -m 0755 /etc/apt/keyrings || handle_error "Failed to create keyrings directory" -curl -s 'https://packages.networkradius.com/pgp/packages%40networkradius.com' | sudo tee /etc/apt/keyrings/packages.networkradius.com.asc > /dev/null || handle_error "Failed to download NetworkRADIUS PGP key" -COMPLETED_STEPS+=("NetworkRADIUS PGP key installed") - -# Add NetworkRADIUS APT preferences -printf 'Package: /freeradius/\nPin: origin "packages.networkradius.com"\nPin-Priority: 999\n' | sudo tee /etc/apt/preferences.d/networkradius > /dev/null || handle_error "Failed to set NetworkRADIUS preferences" - -# Add NetworkRADIUS repository based on Ubuntu version -case $UBUNTU_VERSION in - "noble") - REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/noble noble main" - ;; - "jammy") - REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/jammy jammy main" - ;; - "focal") - REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/focal focal main" - ;; - *) - handle_error "Unsupported Ubuntu version: $UBUNTU_VERSION" - ;; -esac - -echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/packages.networkradius.com.asc] $REPO_URL" | sudo tee /etc/apt/sources.list.d/networkradius.list > /dev/null || handle_error "Failed to add NetworkRADIUS repository" -log_success "NetworkRADIUS repository configured for Ubuntu $UBUNTU_VERSION" -COMPLETED_STEPS+=("NetworkRADIUS repository configured") - -# Set Valkey Repository -log_step "Adding Valkey repository" -if [[ "$UBUNTU_VERSION" == "focal" || "$UBUNTU_VERSION" == "jammy" ]]; then - # Remove conflicting redis packages - apt-get remove -y redis-tools redis-server || true - - # Fetch Percona release package - wget https://repo.percona.com/apt/percona-release_latest.$(lsb_release -sc)_all.deb - - # Install Percona release package - dpkg -i percona-release_latest.$(lsb_release -sc)_all.deb - - # Enable Percona repository for Valkey - percona-release enable valkey experimental - - # Update package list - apt-get update -fi - -COMPLETED_STEPS+=("Valkey repository added") - - - -# Update and upgrade system -log_step "Updating system packages" -apt-get update || handle_error "Failed to update package lists" -apt-get upgrade -y || handle_error "Failed to upgrade packages" -COMPLETED_STEPS+=("System packages updated") - -# Install required packages -log_step "Installing required packages" -if [ "$REINSTALL" = true ]; then - log_info "Reinstalling packages (forcing configuration file replacement)" - apt-get install --reinstall -y -o Dpkg::Options::="--force-confmiss" \ - nginx-full \ - python3-certbot-nginx \ - php${PHP_VERSION}-fpm \ - php${PHP_VERSION}-mysql \ - php${PHP_VERSION}-cli \ - php${PHP_VERSION}-curl \ - php${PHP_VERSION}-zip \ - php${PHP_VERSION}-common \ - php${PHP_VERSION}-gd \ - php${PHP_VERSION}-mbstring \ - php${PHP_VERSION}-xml \ - php${PHP_VERSION}-dev \ - php${PHP_VERSION}-bcmath \ - php${PHP_VERSION}-intl \ - php${PHP_VERSION}-redis \ - php${PHP_VERSION}-tokenizer \ - php${PHP_VERSION}-ctype \ - php${PHP_VERSION}-fileinfo \ - php${PHP_VERSION}-json \ - git \ - unzip \ - curl \ - wget \ - software-properties-common \ - apt-transport-https \ - ca-certificates \ - gnupg \ - lsb-release \ - supervisor \ - valkey \ - valkey-compat \ - openvpn \ - easy-rsa \ - freeradius \ - freeradius-mysql \ - freeradius-utils \ - mariadb-server \ - mariadb-client || handle_error "Failed to reinstall packages" -else - apt-get install -y \ - nginx-full \ - python3-certbot-nginx \ - php${PHP_VERSION}-fpm \ - php${PHP_VERSION}-mysql \ - php${PHP_VERSION}-cli \ - php${PHP_VERSION}-curl \ - php${PHP_VERSION}-zip \ - php${PHP_VERSION}-common \ - php${PHP_VERSION}-gd \ - php${PHP_VERSION}-mbstring \ - php${PHP_VERSION}-xml \ - php${PHP_VERSION}-dev \ - php${PHP_VERSION}-bcmath \ - php${PHP_VERSION}-intl \ - php${PHP_VERSION}-redis \ - php${PHP_VERSION}-tokenizer \ - php${PHP_VERSION}-ctype \ - php${PHP_VERSION}-fileinfo \ - php${PHP_VERSION}-json \ - git \ - unzip \ - curl \ - wget \ - software-properties-common \ - apt-transport-https \ - ca-certificates \ - gnupg \ - lsb-release \ - supervisor \ - valkey \ - valkey-compat \ - openvpn \ - easy-rsa \ - freeradius \ - freeradius-mysql \ - freeradius-utils \ - mariadb-server \ - mariadb-client || handle_error "Failed to install packages" -fi -COMPLETED_STEPS+=("Required packages installed") - -# Configure Valkey with optimal settings for FreeRADIUS -log_step "Configuring Valkey with optimized settings" - -# Calculate optimal memory allocation (75% of available RAM, capped at 3GB, minimum 1GB) -TOTAL_RAM_KB=$(grep MemTotal /proc/meminfo | awk '{print $2}') -TOTAL_RAM_MB=$((TOTAL_RAM_KB / 1024)) -MAX_MEMORY_MB=$((TOTAL_RAM_MB * 75 / 100)) -if [ "$MAX_MEMORY_MB" -gt 3072 ]; then - MAX_MEMORY_MB=3072 # Cap at 3GB -fi -if [ "$MAX_MEMORY_MB" -lt 1024 ]; then - MAX_MEMORY_MB=1024 # Minimum 1GB -fi - -log_info "Configuring Valkey with ${MAX_MEMORY_MB}MB memory allocation" - -# Create Valkey configuration directory if it doesn't exist -mkdir -p /etc/valkey || handle_error "Failed to create Valkey configuration directory" - -# Configure Valkey with optimized settings for FreeRADIUS -cat > /etc/valkey/valkey.conf << EOL -# Valkey configuration for FreeRADIUS - -bind 0.0.0.0 ::0 -protected-mode yes -port 6379 -tcp-backlog 511 -timeout 0 -tcp-keepalive 300 -daemonize no -supervised systemd -pidfile /var/run/valkey/valkey.pid -loglevel notice -logfile /var/log/valkey/valkey.log -databases 16 - -# Memory management -maxmemory ${MAX_MEMORY_MB}mb -maxmemory-policy volatile-lru -maxmemory-samples 5 - -# AOF persistence (enabled for better durability) -appendonly yes -dir /var/lib/valkey -appendfilename "appendonly.aof" -appendfsync everysec -no-appendfsync-on-rewrite no -auto-aof-rewrite-percentage 100 -auto-aof-rewrite-min-size 64mb -aof-load-truncated yes -aof-rewrite-incremental-fsync yes - -# Performance optimizations -stop-writes-on-bgsave-error no -rdbcompression yes -rdbchecksum yes -dbfilename dump.rdb - -# Disable RDB snapshots since we're using AOF -save "" - -# Security -# Reuse the same password as MySQL for simplicity -requirepass "$MYSQL_PASSWORD" - -# Network -tcp-keepalive 300 -repl-timeout 60 -repl-ping-slave-period 10 -repl-backlog-size 1mb -repl-backlog-ttl 3600 - -# Client timeouts -timeout 0 -tcp-keepalive 300 - -# Disable dangerous commands -rename-command FLUSHDB "" -rename-command FLUSHALL "" -rename-command CONFIG "" -rename-command SHUTDOWN "" - -# Set the number of threads to serve the requests -io-threads 2 -io-threads-do-reads yes - -# Set the max number of connected clients at the same time -maxclients 10000 - -# Set the threshold for keys with an expire set to be considered for deletion -active-expire-effort 1 - -# Set the threshold for client output buffer limits -client-output-buffer-limit normal 0 0 0 -client-output-buffer-limit replica 256mb 64mb 60 -client-output-buffer-limit pubsub 32mb 8mb 60 - -# Tune hash data structure -hash-max-ziplist-entries 512 -hash-max-ziplist-value 64 - -# Tune list data structure -list-max-ziplist-size -2 - -# Tune set data structure -set-max-intset-entries 512 - -# Tune zset data structure -zset-max-ziplist-entries 128 -zset-max-ziplist-value 64 - -# Tune hll data structure -hll-sparse-max-bytes 3000 - -# Tune stream data structure -stream-node-max-bytes 4096 -stream-node-max-entries 100 - -# Enable active defragmentation -active-defrag-threshold-lower 10 -active-defrag-threshold-upper 100 -active-defrag-ignore-bytes 100mb -active-defrag-cycle-min 5 -active-defrag-cycle-max 75 -active-defrag-max-scan-fields 1000 -EOL - -# Set proper permissions for Valkey directories -log_step "Setting Valkey directory permissions" -mkdir -p /var/lib/valkey/appendonlydir -chown -R valkey:valkey /var/lib/valkey /var/log/valkey /var/run/valkey -chmod 750 /var/lib/valkey /var/log/valkey /var/run/valkey - -# Fix Valkey service -sed -i 's/ConditionPathExists=!\/etc\/valkey\/REDIS_MIGRATION/ConditionPathExists=\/etc\/valkey\/REDIS_MIGRATION/g' /usr/lib/systemd/system/valkey.service - -# Restart Valkey to apply new configuration -systemctl daemon-reexec || log_warning "daemon-reexec failed (non-critical)" -systemctl daemon-reload || handle_error "Failed to reload systemd daemon" -systemctl restart valkey || handle_error "Failed to restart Valkey" -systemctl enable valkey || handle_error "Failed to enable Valkey" - -# Verify Valkey is running -log_step "Verifying Valkey service status" - -# Check service status -if systemctl is-active --quiet valkey; then - log_success "Valkey service is running" - COMPLETED_STEPS+=("Valkey configured with ${MAX_MEMORY_MB}MB memory allocation") -else - # If service is not running, try to get more information - log_warning "Valkey service is not running as expected. Checking status..." - systemctl status valkey --no-pager || true - - # Try to start the service - log_info "Attempting to start Valkey service..." - if systemctl start valkey; then - log_success "Successfully started Valkey service" - COMPLETED_STEPS+=("Valkey configured with ${MAX_MEMORY_MB}MB memory allocation") - else - # If we still can't start, show detailed error but don't fail the script - log_error "Failed to start Valkey service. Please check the logs with: journalctl -u valkey -n 50" - log_warning "Continuing installation despite Valkey service issue..." - COMPLETED_STEPS+=("Valkey configuration completed but service failed to start") - fi -fi - -# Create Valkey debug script -log_step "Creating Valkey debug script" -cat > /usr/local/bin/valkey-debug.sh << 'EOF' -#!/bin/bash - -VALKEY_HOST="127.0.0.1" -VALKEY_PORT="6379" - -echo "=== Valkey Status ===" -systemctl status valkey --no-pager -l - -echo -e "\n=== Valkey Key Statistics ===" -echo "Total Keys in DB 0: $(valkey-cli -h $VALKEY_HOST -p $VALKEY_PORT dbsize)" - -EOF - -chmod +x /usr/local/bin/valkey-debug.sh - -COMPLETED_STEPS+=("Valkey monitoring configured") - -# Add monitoring cron job -log_step "Adding monitoring cron job" -echo "*/5 * * * * /usr/local/bin/valkey-debug.sh" > cronjob || handle_error "Failed to write monitoring cron job to temporary file" -crontab cronjob || handle_error "Failed to install monitoring cron job" -rm cronjob || handle_error "Failed to remove temporary monitoring cron job file" -COMPLETED_STEPS+=("Monitoring cron job added") - -# Verify Valkey is working -log_step "Verifying Valkey installation" -if ! systemctl is-active --quiet valkey; then -handle_error "Valkey service is not running" -fi - -# Test Valkey connectivity and basic operations -if [ "$(valkey-cli ping)" != "PONG" ]; then -handle_error "Valkey is not responding to ping" -fi - -# Test Valkey write operation -if [ "$(valkey-cli set test_key test_value)" != "OK" ]; then -handle_error "Valkey write operation failed" -fi - -# Test Valkey read operation -TEST_VALUE=$(valkey-cli get test_key) -if [ "$TEST_VALUE" != "test_value" ]; then -handle_error "Valkey read operation failed" -fi - -# Test Valkey delete operation -if [ "$(valkey-cli del test_key)" != "1" ]; then -handle_error "Valkey delete operation failed" -fi - -# Check Valkey info for basic stats -if ! valkey-cli info | grep -q "valkey_version"; then -handle_error "Unable to get Valkey server information" -fi - -COMPLETED_STEPS+=("Valkey functionality verified") - -# Set Default PHP Version -log_step "Setting default PHP version" -update-alternatives --set php /usr/bin/php${PHP_VERSION} || handle_error "Failed to set default PHP version" -COMPLETED_STEPS+=("PHP ${PHP_VERSION} set as default") - -# Install and configure ionCube Loader -log_step "Installing ionCube Loader" - -# Check if ionCube is already installed -if [ -d "/usr/local/ioncube" ] && [ -f "/usr/local/ioncube/ioncube_loader_lin_${PHP_VERSION}.so" ]; then - log_info "ionCube Loader already exists, skipping download and installation" - COMPLETED_STEPS+=("ionCube Loader reused (already exists)") -else - log_info "ionCube Loader not found, downloading and installing" - - # Change to /tmp directory for downloads - cd /tmp || handle_error "Failed to change to /tmp directory" - - # Download and extract ionCube - wget -O ioncube.zip "https://downloads.ioncube.com/loader_downloads/ioncube_loaders_lin_x86-64.zip" || handle_error "Failed to download ionCube" - unzip -q ioncube.zip || handle_error "Failed to extract ionCube" - - # Remove existing ionCube directory if it exists - rm -rf /usr/local/ioncube 2>/dev/null - - # Move the ioncube directory to /usr/local - mv ioncube /usr/local/ || handle_error "Failed to move ionCube to /usr/local" - - COMPLETED_STEPS+=("ionCube Loader downloaded and installed") -fi - -# Create ionCube ini file with absolute path -cat > /etc/php/${PHP_VERSION}/mods-available/ioncube.ini << EOL -zend_extension = /usr/local/ioncube/ioncube_loader_lin_${PHP_VERSION}.so -EOL - -# Enable ionCube for PHP CLI and FPM -ln -sf /etc/php/${PHP_VERSION}/mods-available/ioncube.ini /etc/php/${PHP_VERSION}/cli/conf.d/00-ioncube.ini || handle_error "Failed to enable ionCube for CLI" -ln -sf /etc/php/${PHP_VERSION}/mods-available/ioncube.ini /etc/php/${PHP_VERSION}/fpm/conf.d/00-ioncube.ini || handle_error "Failed to enable ionCube for FPM" - -# Restart PHP-FPM to load ionCube -systemctl restart php${PHP_VERSION}-fpm || handle_error "Failed to restart PHP-FPM" - -# Verify ionCube installation -if php -v | grep -q "ionCube PHP Loader"; then - log_success "ionCube Loader installed and enabled successfully" - COMPLETED_STEPS+=("ionCube Loader installed and configured") -else - handle_error "ionCube Loader installation verification failed" -fi - -# Start and enable MariaDB -log_step "Configuring MariaDB" - -# Initialize MariaDB system database if not already done -if [ ! -d "/var/lib/mysql/mysql" ]; then - log_info "Initializing MariaDB system database" - mysql_install_db --user=mysql --datadir=/var/lib/mysql || handle_error "Failed to initialize MariaDB" -fi - -systemctl start mariadb || handle_error "Failed to start MariaDB" -systemctl enable mariadb || handle_error "Failed to enable MariaDB" - -# Ensure debian-start script exists (recreate if missing) -if [ ! -f "/etc/mysql/debian-start" ]; then - log_info "Creating missing /etc/mysql/debian-start script" - cat > /etc/mysql/debian-start << 'EOF' -#!/bin/bash -# This script is executed by "/etc/init.d/mysql" on every (re)start. - -# Exit if the script is not being run by root -if [ "$(id -u)" != "0" ]; then - echo "This script must be run as root" 1>&2 - exit 1 -fi - -# Exit successfully if mysql is not running -if ! /usr/bin/mysqladmin --defaults-file=/etc/mysql/debian.cnf ping > /dev/null 2>&1; then - exit 0 -fi - -# Exit successfully -exit 0 -EOF - chmod +x /etc/mysql/debian-start || handle_error "Failed to make debian-start executable" - log_success "Created /etc/mysql/debian-start script" -fi - -COMPLETED_STEPS+=("MariaDB initialized, started and enabled") - -# Configure MySQL to allow remote connections and optimize performance -log_step "Configuring MySQL for remote connections and performance" - -# Create MariaDB configuration directory if it doesn't exist -mkdir -p /etc/mysql/mariadb.conf.d/ - -# Configure MariaDB -cat > /etc/mysql/mariadb.conf.d/50-server.cnf << 'EOL' -[mysqld] -user = mysql -pid-file = /run/mysqld/mysqld.pid -socket = /run/mysqld/mysqld.sock -port = 3306 -basedir = /usr -datadir = /var/lib/mysql -tmpdir = /tmp -lc-messages-dir = /usr/share/mysql -lc-messages = en_US -skip-external-locking - -bind-address = 0.0.0.0 - -key_buffer_size = 16M -max_allowed_packet = 16M -thread_stack = 192K -thread_cache_size = 8 - -myisam-recover-options = BACKUP - -query_cache_limit = 1M -query_cache_size = 16M - -expire_logs_days = 10 -max_binlog_size = 100M - -character-set-server = utf8mb4 -collation-server = utf8mb4_general_ci - -# Performance optimizations -innodb_buffer_pool_size = 1G -innodb_log_file_size = 256M -innodb_flush_method = O_DIRECT -innodb_flush_log_at_trx_commit = 2 -innodb_file_per_table = 1 - -[embedded] - -[mariadb] - -[mariadb-10.6] -EOL - -# Restart MariaDB to apply changes -systemctl restart mariadb || handle_error "Failed to restart MariaDB after configuration change" -COMPLETED_STEPS+=("MySQL configured for remote connections") - -# Get server hostname and set email -DOMAIN=$(hostname -f) -EMAIL_ADDRESS="simpluxsolutions@gmail.com" - -# Generate random credentials or reuse existing ones -DB_CREDENTIALS_FILE="/root/db.txt" - -# Check if db.txt exists and contains valid credentials -if [ -f "$DB_CREDENTIALS_FILE" ] && [ -r "$DB_CREDENTIALS_FILE" ]; then - log_step "Found existing database credentials, reusing them" - - # Extract credentials from existing db.txt file - MYSQL_USER=$(grep "^DB_USERNAME=" "$DB_CREDENTIALS_FILE" | cut -d'=' -f2) - MYSQL_PASSWORD=$(grep "^DB_PASSWORD=" "$DB_CREDENTIALS_FILE" | cut -d'=' -f2) - MYSQL_DATABASE=$(grep "^DB_DATABASE=" "$DB_CREDENTIALS_FILE" | cut -d'=' -f2) - - # Validate that we got all required credentials - if [ -n "$MYSQL_USER" ] && [ -n "$MYSQL_PASSWORD" ] && [ -n "$MYSQL_DATABASE" ]; then - log_info "Reusing existing database credentials: User=$MYSQL_USER, Database=$MYSQL_DATABASE" - COMPLETED_STEPS+=("Database credentials reused from existing file") - else - log_info "Existing db.txt file is incomplete, generating new credentials" - MYSQL_USER="user_$(openssl rand -hex 3)" - MYSQL_PASSWORD="$(openssl rand -base64 12)" - MYSQL_DATABASE="radius" - COMPLETED_STEPS+=("New database credentials generated (existing file was incomplete)") - fi -else - log_step "No existing database credentials found, generating new ones" - MYSQL_USER="user_$(openssl rand -hex 3)" - MYSQL_PASSWORD="$(openssl rand -base64 12)" - MYSQL_DATABASE="radius" - COMPLETED_STEPS+=("New database credentials generated") -fi - -# Secure MariaDB installation -log_step "Securing MariaDB installation" -mysql -e "DELETE FROM mysql.user WHERE User='';" || handle_error "Failed to delete anonymous MariaDB user" -mysql -e "DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');" || handle_error "Failed to delete remote MariaDB root user" -mysql -e "DROP DATABASE IF EXISTS test;" || handle_error "Failed to delete test database" -mysql -e "DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%';" || handle_error "Failed to delete test database" -mysql -e "FLUSH PRIVILEGES;" || handle_error "Failed to flush MariaDB privileges" -COMPLETED_STEPS+=("MariaDB installation secured") - -# Create database and user -log_step "Creating database and user" -mysql -e "CREATE DATABASE $MYSQL_DATABASE;" || handle_error "Failed to create database" - -# Create user with access from any host -mysql -e "CREATE USER '$MYSQL_USER'@'%' IDENTIFIED BY '$MYSQL_PASSWORD';" || handle_error "Failed to create database user" - -# Grant privileges for all hosts -mysql -e "GRANT ALL PRIVILEGES ON $MYSQL_DATABASE.* TO '$MYSQL_USER'@'%';" || handle_error "Failed to grant database privileges" - -mysql -e "FLUSH PRIVILEGES;" || handle_error "Failed to flush MariaDB privileges" -COMPLETED_STEPS+=("Database and user created with full access") - -# Install Composer -log_step "Installing Composer" -if ! command -v composer &> /dev/null; then - php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" || handle_error "Failed to download Composer installer" - php composer-setup.php --quiet || handle_error "Failed to install Composer" - rm composer-setup.php || handle_error "Failed to remove Composer installer" - mv composer.phar /usr/local/bin/composer || handle_error "Failed to move Composer to /usr/local/bin" - chmod +x /usr/local/bin/composer || handle_error "Failed to make Composer executable" -fi -COMPLETED_STEPS+=("Composer installed") - - -# Setup Laravel application -log_step "Setting up Laravel application" -LOCAL_PATH="/var/www/html" -REPO_URL="$GITHUB_REPO_URL" - - -# Remove existing web root if it exists (no backup) -if [ -d "$LOCAL_PATH" ]; then - rm -rf "$LOCAL_PATH" || handle_error "Failed to remove existing web root" -fi - -# Clone the repository -git clone "$REPO_URL" "$LOCAL_PATH" || handle_error "Failed to clone repository" -cd "$LOCAL_PATH" || handle_error "Failed to change directory to web root" - -# Install Laravel dependencies -log_step "Installing Laravel dependencies" -COMPOSER_ALLOW_SUPERUSER=1 composer install --no-interaction --no-security-blocking --prefer-dist || handle_error "Failed to install Laravel dependencies" -COMPLETED_STEPS+=("Laravel dependencies installed") - -# Create and configure .env file -log_step "Configuring .env file" -cp .env.example .env || handle_error "Failed to copy .env.example to .env" -php artisan key:generate --force || handle_error "Failed to generate Laravel key" -COMPLETED_STEPS+=(".env file configured") - -# Update .env with database credentials -sed -i "s|DB_HOST=.*|DB_HOST=localhost|" .env || handle_error "Failed to update DB_HOST in .env" -sed -i "s|DB_PORT=.*|DB_PORT=3306|" .env || handle_error "Failed to update DB_PORT in .env" -sed -i "s|DB_DATABASE=.*|DB_DATABASE=$MYSQL_DATABASE|" .env || handle_error "Failed to update DB_DATABASE in .env" -sed -i "s|DB_USERNAME=.*|DB_USERNAME=$MYSQL_USER|" .env || handle_error "Failed to update DB_USERNAME in .env" -sed -i "s|DB_PASSWORD=.*|DB_PASSWORD=$MYSQL_PASSWORD|" .env || handle_error "Failed to update DB_PASSWORD in .env" -sed -i "s|APP_URL=.*|APP_URL=https://$DOMAIN|" .env || handle_error "Failed to update APP_URL in .env" -sed -i "s|APP_NAME=.*|APP_NAME=\"$DOMAIN\"|" .env || handle_error "Failed to update APP_NAME in .env" -COMPLETED_STEPS+=(".env file updated with database credentials") - - -# Restart services -log_step "Restarting services" -systemctl restart mariadb || handle_error "Failed to restart MariaDB" -systemctl restart php${PHP_VERSION}-fpm || handle_error "Failed to restart PHP ${PHP_VERSION} FPM" -systemctl restart supervisor || handle_error "Failed to restart Supervisor" -systemctl restart openvpn || handle_error "Failed to restart OpenVPN" -COMPLETED_STEPS+=("Services restarted") - -# Run Laravel migrations and seed the database -log_step "Running Laravel migrations and seeding database" -php artisan migrate --force || handle_error "Failed to run Laravel migrations" -php artisan db:seed --force || handle_error "Failed to seed database" -COMPLETED_STEPS+=("Laravel migrations run and database seeded") - -# Optimize RADIUS database indexes -log_step "Optimizing RADIUS database indexes" -cat > /tmp/radius_optimize.sql << "EOL" -USE radius; - --- Add indexes to improve query performance - --- radcheck -ALTER TABLE radcheck - ADD INDEX idx_username (username), - ADD INDEX idx_attribute (attribute); -ANALYZE TABLE radcheck; - --- radreply -ALTER TABLE radreply - ADD INDEX idx_username (username), - ADD INDEX idx_attribute (attribute); -ANALYZE TABLE radreply; - --- radusergroup -ALTER TABLE radusergroup - ADD INDEX idx_username (username), - ADD INDEX idx_groupname (groupname); -ANALYZE TABLE radusergroup; - --- radgroupcheck -ALTER TABLE radgroupcheck - ADD INDEX idx_groupname (groupname), - ADD INDEX idx_attribute (attribute); -ANALYZE TABLE radgroupcheck; - --- radgroupreply -ALTER TABLE radgroupreply - ADD INDEX idx_groupname (groupname), - ADD INDEX idx_attribute (attribute); -ANALYZE TABLE radgroupreply; - --- radacct (very critical for performance) -ALTER TABLE radacct - ADD INDEX idx_username (username), - ADD INDEX idx_acctsessionid (acctsessionid), - ADD INDEX idx_framedipaddress (framedipaddress), - ADD INDEX idx_acctstarttime (acctstarttime), - ADD INDEX idx_acctstoptime (acctstoptime), - ADD INDEX idx_nasipaddress (nasipaddress), - ADD INDEX idx_calledstationid (calledstationid), - ADD INDEX idx_callingstationid (callingstationid); -ANALYZE TABLE radacct; - --- radpostauth -ALTER TABLE radpostauth - ADD INDEX idx_username (username), - ADD INDEX idx_reply (reply), - ADD INDEX idx_authdate (authdate); -ANALYZE TABLE radpostauth; - --- hotspot_sessions -ALTER TABLE hotspot_sessions - ADD INDEX idx_payment_voucher (payment_id, voucher); -ANALYZE TABLE hotspot_sessions; - --- Convert tables to InnoDB and utf8mb4 (recommended for reliability and Unicode support) -ALTER TABLE radcheck ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -ALTER TABLE radreply ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -ALTER TABLE radusergroup ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -ALTER TABLE radgroupcheck ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -ALTER TABLE radgroupreply ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -ALTER TABLE radacct ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -ALTER TABLE radpostauth ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -ALTER TABLE hotspot_sessions ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - --- Analyze again after engine/charset conversion -ANALYZE TABLE radcheck; -ANALYZE TABLE radreply; -ANALYZE TABLE radusergroup; -ANALYZE TABLE radgroupcheck; -ANALYZE TABLE radgroupreply; -ANALYZE TABLE radacct; -ANALYZE TABLE radpostauth; -ANALYZE TABLE hotspot_sessions; - -EOL - -mysql -u root < /tmp/radius_optimize.sql || handle_error "Failed to optimize RADIUS database indexes" -rm -f /tmp/radius_optimize.sql -COMPLETED_STEPS+=("RADIUS database indexes optimized") - -# Configure FreeRADIUS -log_step "Configuring FreeRADIUS" - -# Enable buffered-sql site -log_step "Enabling buffered-sql site" -# Ensure the sites-enabled directory exists -mkdir -p /etc/freeradius/sites-enabled || handle_error "Failed to create FreeRADIUS sites-enabled directory" -ln -sf /etc/freeradius/sites-available/buffered-sql /etc/freeradius/sites-enabled/buffered-sql || handle_error "Failed to enable buffered-sql site" -COMPLETED_STEPS+=("FreeRADIUS buffered-sql site enabled") - -# Enable SQL module for FreeRADIUS -log_step "Enabling SQL module" -# Ensure the mods-enabled directory exists -mkdir -p /etc/freeradius/mods-enabled || handle_error "Failed to create FreeRADIUS mods-enabled directory" -ln -sf /etc/freeradius/mods-available/sql /etc/freeradius/mods-enabled/sql || handle_error "Failed to enable SQL module" -COMPLETED_STEPS+=("FreeRADIUS SQL module enabled") - -# Configure FreeRADIUS REST module -log_step "Configuring FreeRADIUS REST module" -REST_CONFIG="/etc/freeradius/mods-available/rest" - -# REST module disabled for SimpleISP to avoid connection errors during configuration test -#rm /etc/freeradius/mods-enabled/rest || handle_error "Failed to disable REST module" - -if [ -f "$REST_CONFIG" ]; then - # Update connect_uri to use domain/api instead of localhost - sed -i 's|connect_uri = "http://127.0.0.1/"|connect_uri = "https://'$DOMAIN'/api"|g' "$REST_CONFIG" || handle_error "Failed to configure REST module connect_uri" - # Also handle the commented version - sed -i 's|# connect_uri = "http://127.0.0.1/"|connect_uri = "https://'$DOMAIN'/api"|g' "$REST_CONFIG" || true -fi - -COMPLETED_STEPS+=("FreeRADIUS REST module configured (disabled for SimpleISP)") - -# Ensure FreeRADIUS configuration files exist (restore if missing) -log_step "Checking FreeRADIUS radiusd.conf" -if [ ! -f "/etc/freeradius/radiusd.conf" ]; then - log_info "FreeRADIUS configuration files missing, reinstalling and reconfiguring FreeRADIUS package" - - # Purge and reinstall FreeRADIUS to ensure clean configuration - apt-get purge -y freeradius freeradius-common freeradius-config 2>/dev/null || echo "FreeRADIUS not installed" - apt-get autoremove -y 2>/dev/null - apt-get install -y freeradius freeradius-mysql freeradius-utils || handle_error "Failed to reinstall FreeRADIUS" - - # Reconfigure the package to ensure configuration files are created - dpkg-reconfigure -f noninteractive freeradius-config 2>/dev/null || echo "Reconfigure not needed" - - # Verify configuration file was created - if [ ! -f "/etc/freeradius/radiusd.conf" ]; then - # Create a minimal radiusd.conf if still missing - log_info "Creating minimal radiusd.conf configuration" - mkdir -p /etc/freeradius - cat > /etc/freeradius/radiusd.conf << 'EOF' -prefix = /usr -exec_prefix = ${prefix} -sysconfdir = /etc -localstatedir = /var -sbindir = ${exec_prefix}/sbin -logdir = /var/log/freeradius -raddbdir = /etc/freeradius -radacctdir = ${logdir}/radacct - -name = freeradius -confdir = ${raddbdir} -modconfdir = ${confdir}/mods-config -certdir = ${confdir}/certs -cadir = ${confdir}/certs -run_dir = ${localstatedir}/run/${name} - -db_dir = ${raddbdir} - -libdir = /usr/lib/freeradius - -pidfile = ${run_dir}/${name}.pid - -correct_escapes = true - -max_request_time = 30 -cleanup_delay = 5 -max_requests = 16384 - -hostname_lookups = no - -log { - destination = files - colourise = yes - file = ${logdir}/radius.log - syslog_facility = daemon - stripped_names = no - auth = no - auth_badpass = no - auth_goodpass = no - msg_denied = "You are already logged in - access denied" -} - -checkrad = ${sbindir}/checkrad - -security { - allow_core_dumps = no - max_attributes = 200 - reject_delay = 1 - status_server = yes -} - -proxy_requests = yes -$INCLUDE proxy.conf - -$INCLUDE clients.conf - -thread pool { - start_servers = 5 - max_servers = 32 - min_spare_servers = 3 - max_spare_servers = 10 - max_requests_per_server = 0 - auto_limit_acct = no -} - -$INCLUDE sites-enabled/ - -$INCLUDE mods-enabled/ - -policy { - $INCLUDE policy.d/ -} - -instantiate { -} -EOF - chmod 644 /etc/freeradius/radiusd.conf - log_success "Created minimal radiusd.conf configuration" - fi - - # Re-enable modules after reinstallation - if [ -f "/etc/freeradius/mods-available/sql" ]; then - ln -sf /etc/freeradius/mods-available/sql /etc/freeradius/mods-enabled/ || handle_error "Failed to re-enable SQL module" - fi - if [ -f "/etc/freeradius/mods-available/rest" ]; then - # REST module disabled for SimpleISP to avoid connection errors during configuration test - # ln -sf /etc/freeradius/mods-available/rest /etc/freeradius/mods-enabled/rest || handle_error "Failed to re-enable REST module" - log_info "REST module configuration skipped for SimpleISP" - fi - log_success "FreeRADIUS configuration files restored" -fi - -COMPLETED_STEPS+=("Completed checking radiusd.conf") - -# Write new FreeRADIUS SQL module -log_step "Writing new FreeRADIUS SQL module" - -SQL_FILE="/etc/freeradius/mods-available/sql" - -# Backup if it exists -[ -f "$SQL_FILE" ] && cp "$SQL_FILE" "${SQL_FILE}.bak.$(date +%Y%m%d-%H%M%S)" - -cat > "$SQL_FILE" < /etc/supervisor/conf.d/queue-worker.conf << "EOL" -[program:queue-worker] -process_name=%(program_name)s_%(process_num)02d -command=php /var/www/html/artisan queue:work --tries=3 -autostart=true -autorestart=true -stopasgroup=true -killasgroup=true -user=www-data -numprocs=5 -redirect_stderr=true -stdout_logfile=/var/www/html/storage/logs/queue-worker.log -EOL -COMPLETED_STEPS+=("Supervisor configured for queue worker") - -# Install OpenVPN based on Ubuntu version -log_step "Installing OpenVPN" -case $UBUNTU_VERSION in - "focal"|"jammy"|"noble") - echo "Installing OpenVPN for Ubuntu $UBUNTU_VERSION" - export AUTO_INSTALL=y - curl -O https://raw.githubusercontent.com/simpleisp/bash/main/openvpn.sh || handle_error "Failed to download OpenVPN installer" - chmod +x openvpn.sh || handle_error "Failed to make OpenVPN installer executable" - ./openvpn.sh || handle_error "Failed to install OpenVPN" - - # Enable and start OpenVPN service - systemctl enable openvpn || handle_error "Failed to enable OpenVPN service" - systemctl start openvpn || handle_error "Failed to start OpenVPN service" - - # Set more secure permissions for OpenVPN - chown -R root:root /etc/openvpn || handle_error "Failed to set ownership of OpenVPN configuration directory" - chmod -R 777 /etc/openvpn || handle_error "Failed to set permissions of OpenVPN configuration directory" - chmod -R 777 /etc/openvpn/easy-rsa || handle_error "Failed to set permissions of OpenVPN easy-rsa directory" - ;; - *) - handle_error "Unsupported Ubuntu version for OpenVPN installation: $UBUNTU_VERSION" - ;; -esac -COMPLETED_STEPS+=("OpenVPN installed") - -# Configure Nginx -log_step "Configuring Nginx" -cat > /etc/nginx/sites-available/default << EOL -server { - - listen 80; - listen [::]:80; - - root /var/www/html/public; - index index.php index.html index.htm index.nginx-debian.html; - - server_name $DOMAIN; - - location / { - try_files \$uri \$uri/ /index.php?\$query_string; - } - - location ~ \.php$ { - include snippets/fastcgi-php.conf; - fastcgi_pass unix:/var/run/php/php${PHP_VERSION}-fpm.sock; - } - - location ~ /\.ht { - deny all; - } - -} -EOL -COMPLETED_STEPS+=("Nginx configured") - -# Enable the default site -log_step "Enabling Nginx default site" -ln -sf /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default || handle_error "Failed to enable Nginx default site" -COMPLETED_STEPS+=("Nginx default site enabled") - -# Test the Nginx configuration -log_step "Testing Nginx configuration" -nginx -t || handle_error "Failed to test Nginx configuration" -COMPLETED_STEPS+=("Nginx configuration tested") - -# If the configuration is OK, reload and restart Nginx -log_step "Reloading and restarting Nginx" -systemctl reload nginx || handle_error "Failed to reload Nginx" -systemctl restart nginx || handle_error "Failed to restart Nginx" -COMPLETED_STEPS+=("Nginx reloaded and restarted") - -# Set correct permissions -log_step "Setting correct permissions" -chown -R www-data:www-data /var/www/html || handle_error "Failed to set ownership of web root" -chmod -R 775 /var/www/html/storage || handle_error "Failed to set permissions of storage directory" -chmod -R 775 /var/www/html/bootstrap/cache || handle_error "Failed to set permissions of cache directory" - -# Make the scripts executable -chmod +x /var/www/html/sh/set_permissions.sh || handle_error "Failed to make permissions script executable" -chmod +x /var/www/html/sh/restart-services.sh || handle_error "Failed to make services script executable" - -# Run the script once to apply initial configuration -/var/www/html/sh/set_permissions.sh || handle_error "Failed to run permissions script" -/var/www/html/sh/restart-services.sh || handle_error "Failed to run services script" - -COMPLETED_STEPS+=("Correct permissions set") - -# Install cron -log_step "Installing cron" -# Write cron job entry to a temporary file -echo "* * * * * php /var/www/html/artisan schedule:run >> /dev/null 2>&1" > cronjob || handle_error "Failed to write cron job to temporary file" - -# Install the cron job from the temporary file -crontab cronjob || handle_error "Failed to install cron job" -COMPLETED_STEPS+=("Cron job installed") - -# Clean up the temporary file -rm cronjob || handle_error "Failed to remove temporary cron job file" -COMPLETED_STEPS+=("Temporary cron job file removed") - -# Update sudoers for www-data user -log_step "Updating sudoers for www-data user" -cat >> /etc/sudoers << "EOL" -www-data ALL=NOPASSWD: /bin/systemctl start openvpn -www-data ALL=NOPASSWD: /bin/systemctl stop openvpn -www-data ALL=NOPASSWD: /bin/systemctl restart openvpn -www-data ALL=NOPASSWD: /bin/systemctl status openvpn -www-data ALL=NOPASSWD: /bin/systemctl reload openvpn -www-data ALL=NOPASSWD: /bin/systemctl enable openvpn -www-data ALL=NOPASSWD: /bin/systemctl disable openvpn -www-data ALL=NOPASSWD: /bin/systemctl start freeradius -www-data ALL=NOPASSWD: /bin/systemctl stop freeradius -www-data ALL=NOPASSWD: /bin/systemctl restart freeradius -www-data ALL=NOPASSWD: /bin/systemctl status freeradius -www-data ALL=NOPASSWD: /bin/systemctl reload freeradius -www-data ALL=NOPASSWD: /bin/systemctl enable freeradius -www-data ALL=NOPASSWD: /bin/systemctl disable freeradius -www-data ALL=NOPASSWD: /bin/supervisorctl stop all -www-data ALL=NOPASSWD: /bin/supervisorctl reread -www-data ALL=NOPASSWD: /bin/supervisorctl update -www-data ALL=NOPASSWD: /bin/supervisorctl start all -www-data ALL=NOPASSWD: /bin/supervisorctl restart all -www-data ALL=NOPASSWD: /bin/supervisorctl status -www-data ALL=NOPASSWD: /bin/systemctl restart supervisor -www-data ALL=NOPASSWD: /bin/systemctl status ssh -www-data ALL=NOPASSWD: /var/www/html/sh/set_permissions.sh -www-data ALL=NOPASSWD: /var/www/html/sh/restart-services.sh -EOL -COMPLETED_STEPS+=("Sudoers updated for www-data user") - -# Save database credentials -log_step "Saving database credentials" -echo "MySQL Credentials:" > "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" -echo "DB_HOST=localhost" >> "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" -echo "DB_PORT=3306" >> "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" -echo "DB_DATABASE=$MYSQL_DATABASE" >> "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" -echo "DB_USERNAME=$MYSQL_USER" >> "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" -echo "DB_PASSWORD=$MYSQL_PASSWORD" >> "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" -COMPLETED_STEPS+=("Database credentials saved") - -# Start and enable all services -log_step "Starting and enabling all services" -systemctl start nginx || handle_error "Failed to start Nginx" -systemctl enable nginx || handle_error "Failed to enable Nginx" -systemctl start php${PHP_VERSION}-fpm || handle_error "Failed to start PHP ${PHP_VERSION} FPM" -systemctl enable php${PHP_VERSION}-fpm || handle_error "Failed to enable PHP ${PHP_VERSION} FPM" -systemctl start supervisor || handle_error "Failed to start Supervisor" -systemctl enable supervisor || handle_error "Failed to enable Supervisor" -systemctl start openvpn || handle_error "Failed to start OpenVPN" -systemctl enable openvpn || handle_error "Failed to enable OpenVPN" -COMPLETED_STEPS+=("Core services started and enabled") - -# Restart all services to ensure proper configuration -log_step "Restarting all services" -systemctl restart nginx || handle_error "Failed to restart Nginx" -systemctl restart php${PHP_VERSION}-fpm || handle_error "Failed to restart PHP ${PHP_VERSION} FPM" -systemctl restart supervisor || handle_error "Failed to restart Supervisor" -systemctl restart openvpn || handle_error "Failed to restart OpenVPN" -COMPLETED_STEPS+=("Services restarted") - -# Open Firewall Ports and enable ufw -log_step "Opening firewall ports and enabling ufw" -ufw allow ssh || handle_error "Failed to allow SSH through firewall" -ufw allow 9080/tcp || handle_error "Failed to allow port 9080 through firewall" -ufw allow http || handle_error "Failed to allow HTTP through firewall" -ufw allow https || handle_error "Failed to allow HTTPS through firewall" -ufw allow 1194/tcp || handle_error "Failed to allow OpenVPN through firewall" -ufw allow 1812:1813/udp || handle_error "Failed to allow FreeRADIUS through firewall" -ufw reload || handle_error "Failed to reload firewall rules" -yes | ufw enable || handle_error "Failed to enable firewall" -COMPLETED_STEPS+=("Firewall ports opened and ufw enabled") - -# Configure SSL with Certbot -log_step "Configuring SSL with Certbot" -echo "Configuring SSL certificate for $DOMAIN" - -# Check if SSL certificate already exists for this domain -if [ -d "/etc/letsencrypt/live/$DOMAIN" ]; then - log_info "SSL certificate already exists for $DOMAIN, configuring it in Nginx" - # Configure existing certificate in Nginx - certbot --nginx -d "$DOMAIN" --agree-tos --email "$EMAIL_ADDRESS" --no-eff-email --non-interactive --redirect || handle_error "Failed to configure existing SSL certificate in Nginx" - COMPLETED_STEPS+=("SSL certificate configured in Nginx (existing certificate)") -else - log_info "No existing SSL certificate found for $DOMAIN, requesting new certificate" - certbot --nginx -d "$DOMAIN" --agree-tos --email "$EMAIL_ADDRESS" --no-eff-email --non-interactive --redirect || handle_error "Failed to configure SSL with Certbot" - COMPLETED_STEPS+=("SSL configured with Certbot") -fi - -# Create cleanup script for uninstalling all software -log_step "Creating cleanup script" -cat > /root/clean_server.sh << 'EOL' -#!/bin/bash - -# Cleanup script for SimpleISP/SimpleSpot -# This script will uninstall all software installed by the SimpleISP/SimpleSpot installer -# and clean the server for reinstallation - -echo "[$(date)] Starting cleanup process..." - -# Function to handle errors -handle_error() { - echo "[$(date)] ERROR: $1" - exit 1 -} - -# Function to log steps -log_step() { - echo "[$(date)] STEP: $1" -} - -# Confirm before proceeding -echo "WARNING: This will remove ALL software installed by SimpleISP/SimpleSpot and delete all data." -echo "This action CANNOT be undone!" -read -p "Are you sure you want to proceed? (y/n): " -n 1 -r -echo -if [[ ! $REPLY =~ ^[Yy]$ ]]; then - echo "Cleanup cancelled." - exit 0 -fi - -# Detect PHP version (try common versions) -PHP_VERSION="" -for version in 8.2 8.1 8.0 7.4; do - if command -v php${version} &> /dev/null; then - PHP_VERSION=$version - break - fi -done - -if [ -z "$PHP_VERSION" ]; then - echo "Warning: Could not detect PHP version, using 8.2 as default" - PHP_VERSION="8.2" -fi - -echo "[$(date)] Detected PHP version: $PHP_VERSION" - -# Stop services -log_step "Stopping services" -systemctl stop nginx freeradius mariadb valkey php${PHP_VERSION}-fpm supervisor openvpn || echo "Could not stop all services" - -# Remove web files -log_step "Removing web files" -rm -rf /var/www/html/* 2>/dev/null -rm -rf /var/www/html/.* 2>/dev/null - -# Remove configuration directories -log_step "Removing configuration directories" -rm -rf /etc/nginx/sites-available/default 2>/dev/null -rm -rf /etc/nginx/sites-enabled/default 2>/dev/null -rm -rf /etc/freeradius 2>/dev/null -rm -rf /etc/openvpn 2>/dev/null -rm -rf /etc/supervisor 2>/dev/null - -# Remove Redis data -log_step "Removing Redis data" -rm -rf /var/lib/redis/* 2>/dev/null -rm -rf /var/lib/redis/.* 2>/dev/null - -# Remove MySQL/MariaDB data and users -log_step "Removing MySQL/MariaDB data and users" -systemctl stop mariadb 2>/dev/null || echo "MariaDB was not running" -mysql -e "DROP USER IF EXISTS 'simpleisp'@'%';" 2>/dev/null || echo "Could not remove simpleisp user" -mysql -e "DROP USER IF EXISTS 'radius'@'%';" 2>/dev/null || echo "Could not remove radius user" -mysql -e "DROP DATABASE IF EXISTS radius;" 2>/dev/null || echo "Could not remove radius database" -mysql -e "FLUSH PRIVILEGES;" 2>/dev/null - -# Remove MySQL/MariaDB files -log_step "Removing MySQL/MariaDB files" -systemctl stop mariadb 2>/dev/null || echo "MariaDB already stopped" -rm -rf /var/lib/mysql 2>/dev/null -mkdir -p /var/lib/mysql -chown mysql:mysql /var/lib/mysql -rm -rf /run/mysqld 2>/dev/null -rm -f /root/.my.cnf 2>/dev/null -rm -f /root/.mysql_history 2>/dev/null - -# Remove log files -log_step "Removing log files" -rm -f /var/log/nginx/access.log 2>/dev/null -rm -f /var/log/nginx/error.log 2>/dev/null -rm -f /var/log/freeradius/radius.log 2>/dev/null - -# Remove SSL certificates -log_step "Preserving SSL certificates (not removing for reuse on reinstall)" -# rm -rf /etc/letsencrypt/live/* 2>/dev/null -# rm -rf /etc/letsencrypt/archive/* 2>/dev/null -# rm -rf /etc/letsencrypt/renewal/* 2>/dev/null - -# Remove application-specific files -log_step "Removing application-specific files" -# Preserve db.txt for credential reuse on reinstall -# rm -f /root/db.txt 2>/dev/null -rm -f /etc/cron.d/laravel-scheduler 2>/dev/null - -# Remove ionCube files -log_step "Removing ionCube files" -rm -f /etc/php/${PHP_VERSION}/mods-available/ioncube.ini 2>/dev/null -rm -f /etc/php/${PHP_VERSION}/cli/conf.d/00-ioncube.ini 2>/dev/null -rm -f /etc/php/${PHP_VERSION}/fpm/conf.d/00-ioncube.ini 2>/dev/null -# Preserve ionCube installation for reuse on reinstall -# rm -rf /usr/local/ioncube 2>/dev/null -find /usr/lib/php/ -name "*ioncube*" -delete 2>/dev/null - -# Remove temporary files -log_step "Removing temporary files" -rm -rf /tmp/ioncube* 2>/dev/null -rm -f /tmp/*.zip 2>/dev/null -rm -f /tmp/*.tar.gz 2>/dev/null - -# Create cleanup marker -log_step "Creating cleanup marker" -date '+%Y-%m-%d %H:%M:%S' > "/root/.simpleisp_cleanup_done" -echo "[$(date)] Cleanup completed. Marker created at /root/.simpleisp_cleanup_done" - -echo "[$(date)] Cleanup completed. The server is now clean and ready for reinstallation." -EOL - -# Make the cleanup script executable -chmod +x /root/clean_server.sh || handle_error "Failed to make cleanup script executable" -log_success "Cleanup script created at /root/clean_server.sh" -COMPLETED_STEPS+=("Cleanup script created at /root/clean_server.sh") - -log_success "Installation completed successfully!" -echo "You can find your database credentials in $DB_CREDENTIALS_FILE" -echo "Your SimpleISP installation is available at: https://$DOMAIN" -echo "Installation logs are available at: $INSTALL_LOG" -echo "To uninstall everything and clean the server, run: /root/clean_server.sh" diff --git a/ubuntu_simplespot_old.sh b/ubuntu_simplespot_old.sh deleted file mode 100644 index ce73d8c..0000000 --- a/ubuntu_simplespot_old.sh +++ /dev/null @@ -1,1451 +0,0 @@ -#!/bin/bash - -# Setup logging and error handling -INSTALL_LOG="/root/install.txt" -STEP_COUNT=0 -COMPLETED_STEPS=() - -# Get server hostname and set email -DOMAIN=$(hostname -f) -EMAIL_ADDRESS="simpluxsolutions@gmail.com" - -# Configuration variables - ONLY DIFFERENCES BETWEEN SCRIPTS -GITHUB_REPO_URL="https://github.com/simpleisp/simplespot.git" -PHP_VERSION="8.2" - -# Logging functions -log_info() { - echo "ℹ️ INFO: $1" | tee -a "$INSTALL_LOG" -} - -log_success() { - echo "✅ SUCCESS: $1" | tee -a "$INSTALL_LOG" -} - -log_error() { - echo "❌ ERROR: $1" | tee -a "$INSTALL_LOG" -} - -log_step() { - STEP_COUNT=$((STEP_COUNT + 1)) - echo "👉 STEP $STEP_COUNT: $1" | tee -a "$INSTALL_LOG" -} - -handle_error() { - log_error "$1" - echo -e "\nCompleted steps before failure:" - printf '%s\n' "${COMPLETED_STEPS[@]}" - echo -e "\nCheck $INSTALL_LOG for more details" - exit 1 -} - -# Initialize log file -touch "$INSTALL_LOG" || { echo "Cannot create log file"; exit 1; } -echo "SimpleISP Installation Log - $(date '+%Y-%m-%d %H:%M:%S')" > "$INSTALL_LOG" -echo "----------------------------------------" >> "$INSTALL_LOG" - -# Configure system for Valkey (memory overcommit and other optimizations) -log_step "Configuring system for Valkey" - -# Enable memory overcommit -if ! grep -q "^vm.overcommit_memory" /etc/sysctl.conf; then - echo "vm.overcommit_memory = 1" | tee -a /etc/sysctl.conf - sysctl -p /etc/sysctl.conf - log_info "Enabled memory overcommit in sysctl" -else - log_info "Memory overcommit already configured in sysctl" -fi -COMPLETED_STEPS+=("System configured for Valkey") - -# Check for cleanup marker file -CLEANUP_MARKER="/root/.simpleisp_cleanup_done" -REINSTALL=false - -if [ -f "$CLEANUP_MARKER" ]; then - log_info "Detected previous cleanup ($(cat $CLEANUP_MARKER))" - log_info "Forcing reinstallation of critical directories and files" - REINSTALL=true - - # Remove the marker file after handling it - rm -f "$CLEANUP_MARKER" - log_success "Cleanup marker processed and removed" -fi - -# Ensure script runs as root -log_step "Checking root privileges" -if [ "$EUID" -ne 0 ]; then - handle_error "Please run as root" -fi -COMPLETED_STEPS+=("Root check passed") - -# Get Ubuntu version -log_step "Detecting Ubuntu version" -UBUNTU_VERSION=$(lsb_release -cs) || handle_error "Failed to detect Ubuntu version" -log_info "Detected Ubuntu version: $UBUNTU_VERSION" -COMPLETED_STEPS+=("Ubuntu version detected: $UBUNTU_VERSION") - -# Set PHP Repo for Ubuntu 24.04 (Noble) -log_step "Adding PHP repository" -if [ "$UBUNTU_VERSION" = "noble" ]; then - # Set up PHP repository for Ubuntu 24.04 - gpgKey='B8DC7E53946656EFBCE4C1DD71DAEAAB4AD4CAB6' - gpgKeyPath='/etc/apt/keyrings/ondrej-ubuntu-php.gpg' - gpgURL="https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x${gpgKey}" - - # Create keyrings directory if it doesn't exist - install -d -m 0755 /etc/apt/keyrings || handle_error "Failed to create keyrings directory" - - # Download and set up GPG key - curl "${gpgURL}" | gpg --dearmor | tee ${gpgKeyPath} >/dev/null || handle_error "Failed to setup PHP GPG key" - gpg --dry-run --quiet --import --import-options import-show ${gpgKeyPath} - - # Create the sources file for PHP repository - cat > /etc/apt/sources.list.d/ondrej-ubuntu-php-noble.sources << 'EOL' -Types: deb -URIs: https://ppa.launchpadcontent.net/ondrej/php/ubuntu/ -Suites: noble -Components: main -Signed-By: /etc/apt/keyrings/ondrej-ubuntu-php.gpg -EOL -else - # For other Ubuntu versions, use the traditional PPA method - LC_ALL=C.UTF-8 add-apt-repository ppa:ondrej/php -y || handle_error "Failed to add PHP repository" -fi -COMPLETED_STEPS+=("PHP repository added") - -# Set NetworkRADIUS PGP public key -log_step "Configuring NetworkRADIUS repository" -install -d -o root -g root -m 0755 /etc/apt/keyrings || handle_error "Failed to create keyrings directory" -curl -s 'https://packages.networkradius.com/pgp/packages%40networkradius.com' | sudo tee /etc/apt/keyrings/packages.networkradius.com.asc > /dev/null || handle_error "Failed to download NetworkRADIUS PGP key" -COMPLETED_STEPS+=("NetworkRADIUS PGP key installed") - -# Add NetworkRADIUS APT preferences -printf 'Package: /freeradius/\nPin: origin "packages.networkradius.com"\nPin-Priority: 999\n' | sudo tee /etc/apt/preferences.d/networkradius > /dev/null || handle_error "Failed to set NetworkRADIUS preferences" - -# Add NetworkRADIUS repository based on Ubuntu version -case $UBUNTU_VERSION in - "noble") - REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/noble noble main" - ;; - "jammy") - REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/jammy jammy main" - ;; - "focal") - REPO_URL="http://packages.networkradius.com/freeradius-3.2/ubuntu/focal focal main" - ;; - *) - handle_error "Unsupported Ubuntu version: $UBUNTU_VERSION" - ;; -esac - -echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/packages.networkradius.com.asc] $REPO_URL" | sudo tee /etc/apt/sources.list.d/networkradius.list > /dev/null || handle_error "Failed to add NetworkRADIUS repository" -log_success "NetworkRADIUS repository configured for Ubuntu $UBUNTU_VERSION" -COMPLETED_STEPS+=("NetworkRADIUS repository configured") - -# Set environment variable to avoid interactive prompts -export DEBIAN_FRONTEND=noninteractive - -# Update and upgrade system -log_step "Updating system packages" -apt-get update || handle_error "Failed to update package lists" -apt-get upgrade -y || handle_error "Failed to upgrade packages" -COMPLETED_STEPS+=("System packages updated") - -# Install required packages -log_step "Installing required packages" -if [ "$REINSTALL" = true ]; then - log_info "Reinstalling packages (forcing configuration file replacement)" - apt-get install --reinstall -y -o Dpkg::Options::="--force-confmiss" \ - nginx-full \ - python3-certbot-nginx \ - php${PHP_VERSION}-fpm \ - php${PHP_VERSION}-mysql \ - php${PHP_VERSION}-cli \ - php${PHP_VERSION}-curl \ - php${PHP_VERSION}-zip \ - php${PHP_VERSION}-common \ - php${PHP_VERSION}-gd \ - php${PHP_VERSION}-mbstring \ - php${PHP_VERSION}-xml \ - php${PHP_VERSION}-dev \ - php${PHP_VERSION}-bcmath \ - php${PHP_VERSION}-intl \ - php${PHP_VERSION}-redis \ - php${PHP_VERSION}-tokenizer \ - php${PHP_VERSION}-ctype \ - php${PHP_VERSION}-fileinfo \ - git \ - unzip \ - curl \ - wget \ - software-properties-common \ - apt-transport-https \ - ca-certificates \ - gnupg \ - lsb-release \ - supervisor \ - valkey-server \ - valkey-tools \ - valkey-redis-compat \ - valkey-sentinel \ - ufw \ - openvpn \ - easy-rsa \ - freeradius \ - freeradius-mysql \ - freeradius-utils \ - freeradius-rest \ - mariadb-server \ - mariadb-client || handle_error "Failed to reinstall packages" -else - apt-get install -y \ - nginx-full \ - python3-certbot-nginx \ - php${PHP_VERSION}-fpm \ - php${PHP_VERSION}-mysql \ - php${PHP_VERSION}-cli \ - php${PHP_VERSION}-curl \ - php${PHP_VERSION}-zip \ - php${PHP_VERSION}-common \ - php${PHP_VERSION}-gd \ - php${PHP_VERSION}-mbstring \ - php${PHP_VERSION}-xml \ - php${PHP_VERSION}-dev \ - php${PHP_VERSION}-bcmath \ - php${PHP_VERSION}-intl \ - php${PHP_VERSION}-redis \ - php${PHP_VERSION}-tokenizer \ - php${PHP_VERSION}-ctype \ - php${PHP_VERSION}-fileinfo \ - git \ - unzip \ - curl \ - wget \ - software-properties-common \ - apt-transport-https \ - ca-certificates \ - gnupg \ - lsb-release \ - supervisor \ - valkey-server \ - valkey-tools \ - valkey-redis-compat \ - valkey-sentinel \ - ufw \ - openvpn \ - easy-rsa \ - mariadb-server \ - mariadb-client \ - freeradius \ - freeradius-mysql \ - freeradius-utils \ - freeradius-rest || handle_error "Failed to install packages" -fi -COMPLETED_STEPS+=("Required packages installed") - -# Configure Valkey service overrides -log_step "Configuring Valkey service overrides" - -VKEY_OVERRIDE_DIR="/etc/systemd/system/valkey.service.d" -VKEY_OVERRIDE_FILE="${VKEY_OVERRIDE_DIR}/override.conf" - -mkdir -p "$VKEY_OVERRIDE_DIR" -cat > "$VKEY_OVERRIDE_FILE" << 'EOF' -[Unit] - -[Service] -# Increase timeouts to prevent premature termination -TimeoutStartSec=300 -TimeoutStopSec=300 - -# Ensure service restarts on failure -Restart=always -RestartSec=10s - -# Disable OOM killer for Valkey -OOMScoreAdjust=-1000 -EOF - -# Apply changes and restart Valkey -log_info "Applying Valkey service configuration..." - -# Configure Valkey with optimal settings for FreeRADIUS -log_step "Configuring Valkey with optimized settings" - -# Calculate optimal memory allocation (75% of available RAM, capped at 3GB, minimum 1GB) -TOTAL_RAM_KB=$(grep MemTotal /proc/meminfo | awk '{print $2}') -TOTAL_RAM_MB=$((TOTAL_RAM_KB / 1024)) -MAX_MEMORY_MB=$((TOTAL_RAM_MB * 75 / 100)) -if [ "$MAX_MEMORY_MB" -gt 3072 ]; then - MAX_MEMORY_MB=3072 # Cap at 3GB -fi -if [ "$MAX_MEMORY_MB" -lt 1024 ]; then - MAX_MEMORY_MB=1024 # Minimum 1GB -fi - -log_info "Configuring Valkey with ${MAX_MEMORY_MB}MB memory allocation" - -# Create Valkey configuration directory if it doesn't exist -mkdir -p /etc/valkey || handle_error "Failed to create Valkey configuration directory" - -# Configure Valkey with optimized settings for FreeRADIUS -cat > /etc/valkey/valkey.conf << EOL -# Valkey configuration for FreeRADIUS - -bind 0.0.0.0 ::0 -protected-mode yes -port 6379 -tcp-backlog 511 -timeout 0 -tcp-keepalive 300 -daemonize no -supervised systemd -pidfile /var/run/valkey/valkey.pid -loglevel notice -logfile /var/log/valkey/valkey.log -databases 16 - -# Memory management -maxmemory ${MAX_MEMORY_MB}mb -maxmemory-policy volatile-lru -maxmemory-samples 5 - -# AOF persistence (enabled for better durability) -appendonly yes -dir /var/lib/valkey -appendfilename "appendonly.aof" -appendfsync everysec -no-appendfsync-on-rewrite no -auto-aof-rewrite-percentage 100 -auto-aof-rewrite-min-size 64mb -aof-load-truncated yes -aof-rewrite-incremental-fsync yes - -# Performance optimizations -stop-writes-on-bgsave-error no -rdbcompression yes -rdbchecksum yes -dbfilename dump.rdb - -# Disable RDB snapshots since we're using AOF -save "" - -# Security -# Reuse the same password as MySQL for simplicity -requirepass "$MYSQL_PASSWORD" - -# Network -tcp-keepalive 300 -repl-timeout 60 -repl-ping-slave-period 10 -repl-backlog-size 1mb -repl-backlog-ttl 3600 - -# Client timeouts -timeout 0 -tcp-keepalive 300 - -# Disable dangerous commands -rename-command FLUSHDB "" -rename-command FLUSHALL "" -rename-command CONFIG "" -rename-command SHUTDOWN "" - -# Set the number of threads to serve the requests -io-threads 2 -io-threads-do-reads yes - -# Set the max number of connected clients at the same time -maxclients 10000 - -# Set the threshold for keys with an expire set to be considered for deletion -active-expire-effort 1 - -# Set the threshold for client output buffer limits -client-output-buffer-limit normal 0 0 0 -client-output-buffer-limit replica 256mb 64mb 60 -client-output-buffer-limit pubsub 32mb 8mb 60 - -# Tune hash data structure -hash-max-ziplist-entries 512 -hash-max-ziplist-value 64 - -# Tune list data structure -list-max-ziplist-size -2 - -# Tune set data structure -set-max-intset-entries 512 - -# Tune zset data structure -zset-max-ziplist-entries 128 -zset-max-ziplist-value 64 - -# Tune hll data structure -hll-sparse-max-bytes 3000 - -# Tune stream data structure -stream-node-max-bytes 4096 -stream-node-max-entries 100 - -# Enable active defragmentation -active-defrag-threshold-lower 10 -active-defrag-threshold-upper 100 -active-defrag-ignore-bytes 100mb -active-defrag-cycle-min 5 -active-defrag-cycle-max 75 -active-defrag-max-scan-fields 1000 -EOL - -# Set proper permissions for Valkey directories -# log_step "Setting Valkey directory permissions" -# mkdir -p /var/lib/valkey/appendonlydir -# chown -R valkey:valkey /var/lib/valkey /var/log/valkey /var/run/valkey -# chmod 750 /var/lib/valkey /var/log/valkey /var/run/valkey - -# Fix Valkey service -#sed -i 's/ConditionPathExists=!\/etc\/valkey\/REDIS_MIGRATION/ConditionPathExists=\/etc\/valkey\/REDIS_MIGRATION/g' /usr/lib/systemd/system/valkey-server.service -#touch /etc/valkey/REDIS_MIGRATION - -# # Set permissions for AOF directory if it exists -# if [ -d "/var/lib/valkey/appendonlydir" ]; then -# chmod 750 /var/lib/valkey/appendonlydir -# chown -R valkey:valkey /var/lib/valkey/appendonlydir -# fi - -# Restart Valkey to apply new configuration -systemctl daemon-reexec || log_warning "daemon-reexec failed (non-critical)" -systemctl daemon-reload || handle_error "Failed to reload systemd daemon" -systemctl restart valkey-server || handle_error "Failed to restart Valkey" -systemctl enable valkey-server || handle_error "Failed to enable Valkey" - -# Verify Valkey is running -log_step "Verifying Valkey service status" - -# Check service status -if systemctl is-active --quiet valkey-server; then - log_success "Valkey service is running" - COMPLETED_STEPS+=("Valkey configured with ${MAX_MEMORY_MB}MB memory allocation") -else - # If service is not running, try to get more information - log_warning "Valkey service is not running as expected. Checking status..." - systemctl status valkey-server --no-pager || true - - # Try to start the service - log_info "Attempting to start Valkey service..." - if systemctl start valkey-server; then - log_success "Successfully started Valkey service" - COMPLETED_STEPS+=("Valkey configured with ${MAX_MEMORY_MB}MB memory allocation") - else - # If we still can't start, show detailed error but don't fail the script - log_error "Failed to start Valkey service. Please check the logs with: journalctl -u valkey-server -n 50" - log_warning "Continuing installation despite Valkey service issue..." - COMPLETED_STEPS+=("Valkey configuration completed but service failed to start") - fi -fi - -# Create Valkey debug script -log_step "Creating Valkey debug script" -cat > /usr/local/bin/valkey-debug.sh << 'EOF' -#!/bin/bash - -VALKEY_HOST="127.0.0.1" -VALKEY_PORT="6379" - -echo "=== Valkey Status ===" -systemctl status valkey --no-pager -l - -echo -e "\n=== Valkey Key Statistics ===" -echo "Total Keys in DB 0: $(valkey-cli -h $VALKEY_HOST -p $VALKEY_PORT dbsize)" - -EOF - -chmod +x /usr/local/bin/valkey-debug.sh - -COMPLETED_STEPS+=("Valkey monitoring configured") - -# Add monitoring cron job -log_step "Adding monitoring cron job" -echo "*/5 * * * * /usr/local/bin/valkey-debug.sh" > cronjob || handle_error "Failed to write monitoring cron job to temporary file" -crontab cronjob || handle_error "Failed to install monitoring cron job" -rm cronjob || handle_error "Failed to remove temporary monitoring cron job file" -COMPLETED_STEPS+=("Monitoring cron job added") - -# Verify Valkey is working -log_step "Verifying Valkey installation" -if ! systemctl is-active --quiet valkey; then -handle_error "Valkey service is not running" -fi - -# Test Valkey connectivity and basic operations -if [ "$(valkey-cli ping)" != "PONG" ]; then -handle_error "Valkey is not responding to ping" -fi - -# Test Valkey write operation -if [ "$(valkey-cli set test_key test_value)" != "OK" ]; then -handle_error "Valkey write operation failed" -fi - -# Test Valkey read operation -TEST_VALUE=$(valkey-cli get test_key) -if [ "$TEST_VALUE" != "test_value" ]; then -handle_error "Valkey read operation failed" -fi - -# Test Valkey delete operation -if [ "$(valkey-cli del test_key)" != "1" ]; then -handle_error "Valkey delete operation failed" -fi - -# Check Valkey info for basic stats -if ! valkey-cli info | grep -q "valkey_version"; then -handle_error "Unable to get Valkey server information" -fi - -COMPLETED_STEPS+=("Valkey functionality verified") - -# Set Default PHP Version -log_step "Setting default PHP version" -update-alternatives --set php /usr/bin/php${PHP_VERSION} || handle_error "Failed to set default PHP version" -COMPLETED_STEPS+=("PHP ${PHP_VERSION} set as default") - -# Install and configure ionCube Loader -log_step "Installing ionCube Loader" - -# Check if ionCube is already installed -if [ -d "/usr/local/ioncube" ] && [ -f "/usr/local/ioncube/ioncube_loader_lin_${PHP_VERSION}.so" ]; then - log_info "ionCube Loader already exists, skipping download and installation" - COMPLETED_STEPS+=("ionCube Loader reused (already exists)") -else - log_info "ionCube Loader not found, downloading and installing" - - # Change to /tmp directory for downloads - cd /tmp || handle_error "Failed to change to /tmp directory" - - # Download and extract ionCube - wget -O ioncube.zip "https://downloads.ioncube.com/loader_downloads/ioncube_loaders_lin_x86-64.zip" || handle_error "Failed to download ionCube" - unzip -q ioncube.zip || handle_error "Failed to extract ionCube" - - # Remove existing ionCube directory if it exists - rm -rf /usr/local/ioncube 2>/dev/null - - # Move the ioncube directory to /usr/local - mv ioncube /usr/local/ || handle_error "Failed to move ionCube to /usr/local" - - COMPLETED_STEPS+=("ionCube Loader downloaded and installed") -fi - -# Create ionCube ini file with absolute path -cat > /etc/php/${PHP_VERSION}/mods-available/ioncube.ini << EOL -zend_extension = /usr/local/ioncube/ioncube_loader_lin_${PHP_VERSION}.so -EOL - -# Enable ionCube for PHP CLI and FPM -ln -sf /etc/php/${PHP_VERSION}/mods-available/ioncube.ini /etc/php/${PHP_VERSION}/cli/conf.d/00-ioncube.ini || handle_error "Failed to enable ionCube for CLI" -ln -sf /etc/php/${PHP_VERSION}/mods-available/ioncube.ini /etc/php/${PHP_VERSION}/fpm/conf.d/00-ioncube.ini || handle_error "Failed to enable ionCube for FPM" - -# Restart PHP-FPM to load ionCube -systemctl restart php${PHP_VERSION}-fpm || handle_error "Failed to restart PHP-FPM" - -# Verify ionCube installation -if php -v | grep -q "ionCube PHP Loader"; then - log_success "ionCube Loader installed and enabled successfully" - COMPLETED_STEPS+=("ionCube Loader installed and configured") -else - handle_error "ionCube Loader installation verification failed" -fi - -# Start and enable MariaDB -log_step "Configuring MariaDB" - -# Initialize MariaDB system database if not already done -if [ ! -d "/var/lib/mysql/mysql" ]; then - log_info "Initializing MariaDB system database" - mysql_install_db --user=mysql --datadir=/var/lib/mysql || handle_error "Failed to initialize MariaDB" -fi - -systemctl start mariadb || handle_error "Failed to start MariaDB" -systemctl enable mariadb || handle_error "Failed to enable MariaDB" - -# Ensure debian-start script exists (recreate if missing) -if [ ! -f "/etc/mysql/debian-start" ]; then - log_info "Creating missing /etc/mysql/debian-start script" - cat > /etc/mysql/debian-start << 'EOF' -#!/bin/bash -# This script is executed by "/etc/init.d/mysql" on every (re)start. - -# Exit if the script is not being run by root -if [ "$(id -u)" != "0" ]; then - echo "This script must be run as root" 1>&2 - exit 1 -fi - -# Exit successfully if mysql is not running -if ! /usr/bin/mysqladmin --defaults-file=/etc/mysql/debian.cnf ping > /dev/null 2>&1; then - exit 0 -fi - -# Exit successfully -exit 0 -EOF - chmod +x /etc/mysql/debian-start || handle_error "Failed to make debian-start executable" - log_success "Created /etc/mysql/debian-start script" -fi - -COMPLETED_STEPS+=("MariaDB initialized, started and enabled") - -# Configure MySQL to allow remote connections -log_step "Configuring MySQL for remote connections" - -# Create MariaDB configuration directory if it doesn't exist -mkdir -p /etc/mysql/mariadb.conf.d/ - -# Configure MariaDB -cat > /etc/mysql/mariadb.conf.d/50-server.cnf << 'EOL' -# -# These groups are read by MariaDB server. -# Use it for options that only the server (but not clients) should see - -# this is read by the standalone daemon and embedded servers -[server] - -# this is only for the mariadbd daemon -[mariadbd] - -# -# * Basic Settings -# - -user = mysql -pid-file = /run/mysqld/mysqld.pid -basedir = /usr -datadir = /var/lib/mysql -tmpdir = /tmp - -# Broken reverse DNS slows down connections considerably and name resolve is -# safe to skip if there are no "host by domain name" access grants -skip-external-locking - -# Bind to all interfaces for network access -bind-address = 0.0.0.0 - -# -# * Fine Tuning -# - -key_buffer_size = 16M -max_allowed_packet = 16M -thread_stack = 192K -thread_cache_size = 8 - -# This replaces the startup script and checks MyISAM tables if needed -# the first time they are touched -myisam-recover-options = BACKUP - -# -# * Query Cache Configuration -# - -query_cache_limit = 1M -query_cache_size = 16M - -# -# * Logging and Replication -# - -# The following can be used as easy to replay backup logs or for replication. -expire_logs_days = 10 -max_binlog_size = 100M - -# -# * Character sets -# - -# MariaDB default is now utf8 4-byte character set. -character-set-server = utf8mb4 -collation-server = utf8mb4_general_ci - -# -# * InnoDB Performance Optimizations -# - -# Most important is to give InnoDB 80 % of the system RAM for buffer use: -innodb_buffer_pool_size = 1G -innodb_log_file_size = 256M -innodb_flush_method = O_DIRECT -innodb_flush_log_at_trx_commit = 2 -innodb_file_per_table = 1 - -# this is only for embedded server -[embedded] - -# This group is only read by MariaDB servers, not by MySQL. -# If you use the same .cnf file for MySQL and MariaDB, -# you can put MariaDB-only options here -[mariadb] - -EOL - -# Restart MariaDB to apply changes -systemctl restart mariadb || handle_error "Failed to restart MariaDB after configuration change" -COMPLETED_STEPS+=("MySQL configured for remote connections") - -# Generate random credentials or reuse existing ones -DB_CREDENTIALS_FILE="/root/db.txt" - -# Check if db.txt exists and contains valid credentials -if [ -f "$DB_CREDENTIALS_FILE" ] && [ -r "$DB_CREDENTIALS_FILE" ]; then - log_step "Found existing database credentials, reusing them" - - # Extract credentials from existing db.txt file - MYSQL_USER=$(grep "^DB_USERNAME=" "$DB_CREDENTIALS_FILE" | cut -d'=' -f2) - MYSQL_PASSWORD=$(grep "^DB_PASSWORD=" "$DB_CREDENTIALS_FILE" | cut -d'=' -f2) - MYSQL_DATABASE=$(grep "^DB_DATABASE=" "$DB_CREDENTIALS_FILE" | cut -d'=' -f2) - - # Validate that we got all required credentials - if [ -n "$MYSQL_USER" ] && [ -n "$MYSQL_PASSWORD" ] && [ -n "$MYSQL_DATABASE" ]; then - log_info "Reusing existing database credentials: User=$MYSQL_USER, Database=$MYSQL_DATABASE" - COMPLETED_STEPS+=("Database credentials reused from existing file") - else - log_info "Existing db.txt file is incomplete, generating new credentials" - MYSQL_USER="user_$(openssl rand -hex 3)" - MYSQL_PASSWORD="$(openssl rand -base64 12)" - MYSQL_DATABASE="radius" - COMPLETED_STEPS+=("New database credentials generated (existing file was incomplete)") - fi -else - log_step "No existing database credentials found, generating new ones" - MYSQL_USER="user_$(openssl rand -hex 3)" - MYSQL_PASSWORD="$(openssl rand -base64 12)" - MYSQL_DATABASE="radius" - COMPLETED_STEPS+=("New database credentials generated") -fi - -# Save database credentials -log_step "Saving database credentials" -echo "MySQL Credentials:" > "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" -echo "DB_HOST=localhost" >> "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" -echo "DB_PORT=3306" >> "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" -echo "DB_DATABASE=$MYSQL_DATABASE" >> "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" -echo "DB_USERNAME=$MYSQL_USER" >> "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" -echo "DB_PASSWORD=$MYSQL_PASSWORD" >> "$DB_CREDENTIALS_FILE" || handle_error "Failed to write database credentials to file" -COMPLETED_STEPS+=("Database credentials saved") - -# Secure MariaDB installation -log_step "Securing MariaDB installation" -mysql -e "DELETE FROM mysql.user WHERE User='';" || handle_error "Failed to delete anonymous MariaDB user" -mysql -e "DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');" || handle_error "Failed to delete remote MariaDB root user" -mysql -e "DROP DATABASE IF EXISTS test;" || handle_error "Failed to delete test database" -mysql -e "DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%';" || handle_error "Failed to delete test database" -mysql -e "FLUSH PRIVILEGES;" || handle_error "Failed to flush MariaDB privileges" -COMPLETED_STEPS+=("MariaDB installation secured") - -# Create database and user -log_step "Creating database and user" -mysql -e "CREATE DATABASE $MYSQL_DATABASE;" || handle_error "Failed to create database" - -# Create user with access from any host -mysql -e "CREATE USER '$MYSQL_USER'@'%' IDENTIFIED BY '$MYSQL_PASSWORD';" || handle_error "Failed to create database user" - -# Grant privileges for all hosts -mysql -e "GRANT ALL PRIVILEGES ON $MYSQL_DATABASE.* TO '$MYSQL_USER'@'%';" || handle_error "Failed to grant database privileges" - -mysql -e "FLUSH PRIVILEGES;" || handle_error "Failed to flush MariaDB privileges" -COMPLETED_STEPS+=("Database and user created with full access") - -# Install Composer -log_step "Installing Composer" -if ! command -v composer &> /dev/null; then - php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" || handle_error "Failed to download Composer installer" - php composer-setup.php --quiet || handle_error "Failed to install Composer" - rm composer-setup.php || handle_error "Failed to remove Composer installer" - mv composer.phar /usr/local/bin/composer || handle_error "Failed to move Composer to /usr/local/bin" - chmod +x /usr/local/bin/composer || handle_error "Failed to make Composer executable" -fi -COMPLETED_STEPS+=("Composer installed") - -# Configure Nginx -log_step "Configuring Nginx" - -# Backup existing default site -mv /etc/nginx/sites-available/default /etc/nginx/sites-available/default.bak -touch /etc/nginx/sites-available/default -ln -sf /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default - -# Configure Nginx -cat > /etc/nginx/sites-available/default << EOL -server { - - listen 80; - listen [::]:80; - - root /var/www/html/public; - index index.php index.html index.htm index.nginx-debian.html; - - server_name $DOMAIN; - - location / { - try_files \$uri \$uri/ /index.php?\$query_string; - } - - location ~ \.php$ { - include snippets/fastcgi-php.conf; - fastcgi_pass unix:/var/run/php/php${PHP_VERSION}-fpm.sock; - } - - location ~ /\.ht { - deny all; - } - -} -EOL -COMPLETED_STEPS+=("Nginx configured") - -# Configure SSL with Certbot -log_step "Configuring SSL with Certbot" -echo "Configuring SSL certificate for $DOMAIN" - -# Check if SSL certificate already exists for this domain -if [ -d "/etc/letsencrypt/live/$DOMAIN" ]; then - log_info "SSL certificate already exists for $DOMAIN, reusing existing certificate" - echo -e "1\n" | certbot --nginx -d "$DOMAIN" || handle_error "Failed to reuse SSL certificate" - COMPLETED_STEPS+=("SSL certificate reused (already exists)") -else - log_info "No existing SSL certificate found for $DOMAIN, requesting new certificate" - certbot --nginx -d "$DOMAIN" --agree-tos --email "$EMAIL_ADDRESS" --no-eff-email --non-interactive --redirect || handle_error "Failed to configure SSL with Certbot" - COMPLETED_STEPS+=("SSL configured with Certbot") -fi - - -# Test the Nginx configuration -log_step "Restarting Nginx" -nginx -t || handle_error "Nginx configuration failed" -pkill nginx || handle_error "Failed to stop Nginx" -systemctl start nginx || handle_error "Failed to start Nginx" -COMPLETED_STEPS+=("Nginx restarted") - -# Setup Laravel application -log_step "Setting up Laravel application" -LOCAL_PATH="/var/www/html" -REPO_URL="$GITHUB_REPO_URL" - -# Remove existing web root if it exists (no backup) -if [ -d "$LOCAL_PATH" ]; then - rm -rf "$LOCAL_PATH" || handle_error "Failed to remove existing web root" -fi - -# Clone the repository -git clone -b master "$REPO_URL" "$LOCAL_PATH" || handle_error "Failed to clone repository" -cd "$LOCAL_PATH" || handle_error "Failed to change directory to web root" - -# Install Laravel dependencies -log_step "Installing Laravel dependencies" -composer install --no-interaction || handle_error "Failed to install Laravel dependencies" -COMPLETED_STEPS+=("Laravel dependencies installed") - -# Create and configure .env file -log_step "Configuring .env file" -cp .env.example .env || handle_error "Failed to copy .env.example to .env" -php artisan key:generate --force || handle_error "Failed to generate Laravel key" -COMPLETED_STEPS+=(".env file configured") - -# Update .env with database credentials -sed -i "s|DB_HOST=.*|DB_HOST=localhost|" .env || handle_error "Failed to update DB_HOST in .env" -sed -i "s|DB_PORT=.*|DB_PORT=3306|" .env || handle_error "Failed to update DB_PORT in .env" -sed -i "s|DB_DATABASE=.*|DB_DATABASE=$MYSQL_DATABASE|" .env || handle_error "Failed to update DB_DATABASE in .env" -sed -i "s|DB_USERNAME=.*|DB_USERNAME=$MYSQL_USER|" .env || handle_error "Failed to update DB_USERNAME in .env" -sed -i "s|DB_PASSWORD=.*|DB_PASSWORD=$MYSQL_PASSWORD|" .env || handle_error "Failed to update DB_PASSWORD in .env" -sed -i "s|APP_URL=.*|APP_URL=https://$DOMAIN|" .env || handle_error "Failed to update APP_URL in .env" -sed -i "s|APP_NAME=.*|APP_NAME=\"$DOMAIN\"|" .env || handle_error "Failed to update APP_NAME in .env" -COMPLETED_STEPS+=(".env file updated with database credentials") - - -# Run Laravel migrations and seed the database -log_step "Running Laravel migrations and seeding database" -php artisan migrate --force || handle_error "Failed to run Laravel migrations" -php artisan db:seed --force || handle_error "Failed to seed database" -COMPLETED_STEPS+=("Laravel migrations run and database seeded") - -# Set correct www permissions -log_step "Setting correct www permissions" -chown -R www-data:www-data /var/www/html || handle_error "Failed to set ownership of web root" -chmod -R 775 /var/www/html/storage || handle_error "Failed to set permissions of storage directory" -chmod -R 775 /var/www/html/bootstrap/cache || handle_error "Failed to set permissions of cache directory" - -# Make the scripts executable -chmod +x /var/www/html/sh/set_permissions.sh || handle_error "Failed to make permissions script executable" -chmod +x /var/www/html/sh/restart-services.sh || handle_error "Failed to make services script executable" - -# Run the script once to apply initial configuration -/var/www/html/sh/set_permissions.sh || handle_error "Failed to run permissions script" -/var/www/html/sh/restart-services.sh || handle_error "Failed to run services script" - -COMPLETED_STEPS+=("Correct www permissions set") - -# Optimize RADIUS database indexes -log_step "Optimizing RADIUS database indexes" -cat > /tmp/radius_optimize.sql << "EOL" -USE radius; - --- Add indexes to improve query performance - --- radcheck -ALTER TABLE radcheck - ADD INDEX idx_username (username), - ADD INDEX idx_attribute (attribute); -ANALYZE TABLE radcheck; - --- radreply -ALTER TABLE radreply - ADD INDEX idx_username (username), - ADD INDEX idx_attribute (attribute); -ANALYZE TABLE radreply; - --- radusergroup -ALTER TABLE radusergroup - ADD INDEX idx_username (username), - ADD INDEX idx_groupname (groupname); -ANALYZE TABLE radusergroup; - --- radgroupcheck -ALTER TABLE radgroupcheck - ADD INDEX idx_groupname (groupname), - ADD INDEX idx_attribute (attribute); -ANALYZE TABLE radgroupcheck; - --- radgroupreply -ALTER TABLE radgroupreply - ADD INDEX idx_groupname (groupname), - ADD INDEX idx_attribute (attribute); -ANALYZE TABLE radgroupreply; - --- radacct (very critical for performance) -ALTER TABLE radacct - ADD INDEX idx_username (username), - ADD INDEX idx_acctsessionid (acctsessionid), - ADD INDEX idx_framedipaddress (framedipaddress), - ADD INDEX idx_acctstarttime (acctstarttime), - ADD INDEX idx_acctstoptime (acctstoptime), - ADD INDEX idx_nasipaddress (nasipaddress), - ADD INDEX idx_calledstationid (calledstationid), - ADD INDEX idx_callingstationid (callingstationid); -ANALYZE TABLE radacct; - --- radpostauth -ALTER TABLE radpostauth - ADD INDEX idx_username (username), - ADD INDEX idx_reply (reply), - ADD INDEX idx_authdate (authdate); -ANALYZE TABLE radpostauth; - --- Convert tables to InnoDB and utf8mb4 (recommended for reliability and Unicode support) -ALTER TABLE radcheck ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -ALTER TABLE radreply ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -ALTER TABLE radusergroup ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -ALTER TABLE radgroupcheck ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -ALTER TABLE radgroupreply ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -ALTER TABLE radacct ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -ALTER TABLE radpostauth ENGINE=InnoDB, CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - --- Analyze again after engine/charset conversion -ANALYZE TABLE radcheck; -ANALYZE TABLE radreply; -ANALYZE TABLE radusergroup; -ANALYZE TABLE radgroupcheck; -ANALYZE TABLE radgroupreply; -ANALYZE TABLE radacct; -ANALYZE TABLE radpostauth; - -EOL - -mysql -u root < /tmp/radius_optimize.sql || handle_error "Failed to optimize RADIUS database indexes" -rm -f /tmp/radius_optimize.sql -COMPLETED_STEPS+=("RADIUS database indexes optimized") - -# Configure Supervisor for queue worker -log_step "Configuring Supervisor for queue worker" -cat > /etc/supervisor/conf.d/queue-worker.conf << "EOL" -[program:queue-worker] -process_name=%(program_name)s_%(process_num)02d -command=php /var/www/html/artisan queue:work --tries=3 -autostart=true -autorestart=true -stopasgroup=true -killasgroup=true -user=www-data -numprocs=5 -redirect_stderr=true -stdout_logfile=/var/www/html/storage/logs/queue-worker.log -EOL -COMPLETED_STEPS+=("Supervisor configured for queue worker") - -# Install OpenVPN based on Ubuntu version -log_step "Installing OpenVPN" -case $UBUNTU_VERSION in - "focal"|"jammy"|"noble") - echo "Installing OpenVPN for Ubuntu $UBUNTU_VERSION" - export AUTO_INSTALL=y - curl -O https://raw.githubusercontent.com/simpleisp/bash/main/openvpn.sh || handle_error "Failed to download OpenVPN installer" - chmod +x openvpn.sh || handle_error "Failed to make OpenVPN installer executable" - ./openvpn.sh || handle_error "Failed to install OpenVPN" - - # Enable and start OpenVPN service - systemctl enable openvpn || handle_error "Failed to enable OpenVPN service" - systemctl start openvpn || handle_error "Failed to start OpenVPN service" - - # Set more secure permissions for OpenVPN - chown -R root:root /etc/openvpn || handle_error "Failed to set ownership of OpenVPN configuration directory" - chmod -R 777 /etc/openvpn || handle_error "Failed to set permissions of OpenVPN configuration directory" - chmod -R 777 /etc/openvpn/easy-rsa || handle_error "Failed to set permissions of OpenVPN easy-rsa directory" - ;; - *) - handle_error "Unsupported Ubuntu version for OpenVPN installation" - ;; -esac -COMPLETED_STEPS+=("OpenVPN installed") - -# Install cron -log_step "Installing cron" -# Write cron job entry to a temporary file -echo "* * * * * php /var/www/html/artisan schedule:run >> /dev/null 2>&1" > cronjob || handle_error "Failed to write cron job to temporary file" - -# Install the cron job from the temporary file -crontab cronjob || handle_error "Failed to install cron job" -COMPLETED_STEPS+=("Cron job installed") - -# Clean up the temporary file -rm cronjob || handle_error "Failed to remove temporary cron job file" -COMPLETED_STEPS+=("Temporary cron job file removed") - -# Update sudoers for www-data user -log_step "Updating sudoers for www-data user" -cat >> /etc/sudoers << 'EOL' -www-data ALL=NOPASSWD: /bin/systemctl start openvpn -www-data ALL=NOPASSWD: /bin/systemctl stop openvpn -www-data ALL=NOPASSWD: /bin/systemctl restart openvpn -www-data ALL=NOPASSWD: /bin/systemctl status openvpn -www-data ALL=NOPASSWD: /bin/systemctl reload openvpn -www-data ALL=NOPASSWD: /bin/systemctl enable openvpn -www-data ALL=NOPASSWD: /bin/systemctl disable openvpn -www-data ALL=NOPASSWD: /bin/systemctl start freeradius -www-data ALL=NOPASSWD: /bin/systemctl stop freeradius -www-data ALL=NOPASSWD: /bin/systemctl restart freeradius -www-data ALL=NOPASSWD: /bin/systemctl status freeradius -www-data ALL=NOPASSWD: /bin/systemctl reload freeradius -www-data ALL=NOPASSWD: /bin/systemctl enable freeradius -www-data ALL=NOPASSWD: /bin/systemctl disable freeradius -www-data ALL=NOPASSWD: /bin/supervisorctl stop all -www-data ALL=NOPASSWD: /bin/supervisorctl reread -www-data ALL=NOPASSWD: /bin/supervisorctl update -www-data ALL=NOPASSWD: /bin/supervisorctl start all -www-data ALL=NOPASSWD: /bin/supervisorctl restart all -www-data ALL=NOPASSWD: /bin/supervisorctl status -www-data ALL=NOPASSWD: /bin/systemctl restart supervisor -www-data ALL=NOPASSWD: /bin/systemctl status ssh -www-data ALL=NOPASSWD: /var/www/html/sh/set_permissions.sh -www-data ALL=NOPASSWD: /var/www/html/sh/restart-services.sh -EOL -COMPLETED_STEPS+=("Sudoers updated for www-data user") - -# Open Firewall Ports and enable ufw -log_step "Opening firewall ports and enabling ufw" -ufw allow ssh || handle_error "Failed to allow SSH through firewall" -ufw allow 9080/tcp || handle_error "Failed to allow port 9080 through firewall" -ufw allow http || handle_error "Failed to allow HTTP through firewall" -ufw allow https || handle_error "Failed to allow HTTPS through firewall" -ufw allow 1194/tcp || handle_error "Failed to allow OpenVPN through firewall" -ufw allow 1812:1813/udp || handle_error "Failed to allow FreeRADIUS through firewall" -ufw reload || handle_error "Failed to reload firewall rules" -yes | ufw enable || handle_error "Failed to enable firewall" -COMPLETED_STEPS+=("Firewall ports opened and ufw enabled") - -# Test FreeRADIUS configuration -log_step "Checking FreeRADIUS files" - -# Ensure FreeRADIUS configuration files exist (restore if missing) -if [ ! -f "/etc/freeradius/radiusd.conf" ]; then -log_info "FreeRADIUS configuration files missing, reinstalling and reconfiguring FreeRADIUS package" - -# Purge and reinstall FreeRADIUS to ensure clean configuration -apt-get purge -y freeradius freeradius-common freeradius-config 2>/dev/null || echo "FreeRADIUS not installed" -apt-get autoremove -y 2>/dev/null -apt-get install -y freeradius freeradius-mysql freeradius-rest freeradius-config || handle_error "Failed to reinstall FreeRADIUS" - -# Reconfigure the package to ensure configuration files are created -dpkg-reconfigure -f noninteractive freeradius-config 2>/dev/null || echo "Reconfigure not needed" -fi - -log_info "FreeRADIUS configuration not missing, skipping reinstallation...." - -# Verify configuration file was created -if [ ! -f "/etc/freeradius/radiusd.conf" ]; then -# Create a minimal radiusd.conf if still missing -log_info "Creating minimal radiusd.conf configuration" -mkdir -p /etc/freeradius -cat > /etc/freeradius/radiusd.conf << 'EOF' -prefix = /usr -exec_prefix = ${prefix} -sysconfdir = /etc -localstatedir = /var -sbindir = ${exec_prefix}/sbin -logdir = /var/log/freeradius -raddbdir = /etc/freeradius -radacctdir = ${logdir}/radacct - -name = freeradius -confdir = ${raddbdir} -modconfdir = ${confdir}/mods-config -certdir = ${confdir}/certs -cadir = ${confdir}/certs -run_dir = ${localstatedir}/run/${name} - -db_dir = ${raddbdir} - -libdir = /usr/lib/freeradius - -pidfile = ${run_dir}/${name}.pid - -correct_escapes = true - -max_request_time = 30 -cleanup_delay = 5 -max_requests = 16384 - -hostname_lookups = no - -log { -destination = files -colourise = yes -file = ${logdir}/radius.log -syslog_facility = daemon -stripped_names = no -auth = no -auth_badpass = no -auth_goodpass = no -msg_denied = "You are already logged in - access denied" -} - -checkrad = ${sbindir}/checkrad - -security { -allow_core_dumps = no -max_attributes = 200 -reject_delay = 1 -status_server = yes -} - -proxy_requests = yes -$INCLUDE proxy.conf - -$INCLUDE clients.conf - -thread pool { -start_servers = 5 -max_servers = 32 -min_spare_servers = 3 -max_spare_servers = 10 -max_requests_per_server = 0 -auto_limit_acct = no -} - -$INCLUDE sites-enabled/ - -$INCLUDE mods-enabled/ - -policy { -$INCLUDE policy.d/ -} - -instantiate { -} -EOF -chmod 644 /etc/freeradius/radiusd.conf -log_success "Created minimal radiusd.conf configuration" -fi - -# Re-enable modules after reinstallation -if [ -f "/etc/freeradius/mods-available/sql" ]; then - ln -sf /etc/freeradius/mods-available/sql /etc/freeradius/mods-enabled/ || handle_error "Failed to re-enable SQL module" -fi -if [ -f "/etc/freeradius/mods-available/rest" ]; then - log_info "Skipping REST module re-enable; using SQL accounting" -fi -log_success "FreeRADIUS configuration files restored" - -COMPLETED_STEPS+=("Completed checking FreeRADIUS files") - -# Enable buffered-sql site -log_step "Enabling FreeRADIUS buffered-sql site" -# Ensure the sites-enabled directory exists -mkdir -p /etc/freeradius/sites-enabled || handle_error "Failed to create FreeRADIUS sites-enabled directory" -ln -sf /etc/freeradius/sites-available/buffered-sql /etc/freeradius/sites-enabled/buffered-sql || handle_error "Failed to enable buffered-sql site" -COMPLETED_STEPS+=("FreeRADIUS buffered-sql site enabled") - -# Enable SQL module for FreeRADIUS -log_step "Enabling SQL module" -# Ensure the mods-enabled directory exists -mkdir -p /etc/freeradius/mods-enabled || handle_error "Failed to create FreeRADIUS mods-enabled directory" -ln -sf /etc/freeradius/mods-available/sql /etc/freeradius/mods-enabled/sql || handle_error "Failed to enable SQL module" -COMPLETED_STEPS+=("FreeRADIUS SQL module enabled") - -# Write new FreeRADIUS SQL module -log_step "Writing new FreeRADIUS SQL module" - -SQL_FILE="/etc/freeradius/mods-available/sql" - -# Backup if it exists -#[ -f "$SQL_FILE" ] && cp "$SQL_FILE" "${SQL_FILE}.bak.$(date +%Y%m%d-%H%M%S)" - -cat > "$SQL_FILE" < /tmp/new_accounting_block -accounting { -# Update accounting packet by adding the CUI attribute -# cui - -# detail -# daily - -# unix - -# radutmp -# sradutmp - -# sqlippool - -sql - -# if (noop) { -# ok -# } - -# pgsql-voip - -exec - -attr_filter.accounting_response - -# Acct-Type Status-Server { -# -# } -} -EOF - - awk ' - BEGIN { skip = 0 } - /^accounting[ \t]*{/ { print_block = 1; print_file("/tmp/new_accounting_block"); skip = 1; next } - /^[ \t]*}/ { if (skip) { skip = 0; next } } - !skip { print } - function print_file(file) { - while ((getline line < file) > 0) print line; - close(file) - } - ' "$DEFAULT_SITE" > /tmp/tmp_site && mv /tmp/tmp_site "$DEFAULT_SITE" - -else - handle_error "Default site configuration file not found" -fi - -COMPLETED_STEPS+=("FreeRADIUS default site configured") - - -# Start and enable all services -log_step "Starting and enabling all services" -systemctl start nginx || handle_error "Failed to start Nginx" -systemctl enable nginx || handle_error "Failed to enable Nginx" -systemctl start php${PHP_VERSION}-fpm || handle_error "Failed to start PHP ${PHP_VERSION} FPM" -systemctl enable php${PHP_VERSION}-fpm || handle_error "Failed to enable PHP ${PHP_VERSION} FPM" -systemctl start supervisor || handle_error "Failed to start Supervisor" -systemctl enable supervisor || handle_error "Failed to enable Supervisor" -systemctl start openvpn || handle_error "Failed to start OpenVPN" -systemctl enable openvpn || handle_error "Failed to enable OpenVPN" -systemctl start freeradius || handle_error "Failed to start FreeRADIUS" -systemctl enable freeradius || handle_error "Failed to enable FreeRADIUS" -COMPLETED_STEPS+=("All services started and enabled") - -# Restart all services to ensure proper configuration -log_step "Restarting all services" -systemctl restart nginx || handle_error "Failed to restart Nginx" -systemctl restart php${PHP_VERSION}-fpm || handle_error "Failed to restart PHP ${PHP_VERSION} FPM" -systemctl restart supervisor || handle_error "Failed to restart Supervisor" -systemctl restart openvpn || handle_error "Failed to restart OpenVPN" -systemctl restart freeradius || handle_error "Failed to restart FreeRADIUS" -COMPLETED_STEPS+=("All services restarted") - -# Create a script to update memory configurations -cat > /usr/local/bin/update_memory_config.sh << 'EOL' -#!/bin/bash -# This script updates MySQL and Valkey memory configurations based on available system memory - -# Log file -LOG_FILE="/var/log/memory_config_updates.log" -echo "[$(date)] Starting memory configuration update" >> "$LOG_FILE" - -# Function to get total system memory in MB -get_total_memory_mb() { - echo $(free -m | awk '/^Mem:/{print $2}') -} - -# Update MySQL configuration -update_mysql_config() { - local total_mem=$(get_total_memory_mb) - local innodb_buffer_pool_size=$((total_mem * 60 / 100))M - local key_buffer_size=$((total_mem * 10 / 100))M - - echo "[$(date)] Updating MySQL configuration" >> "$LOG_FILE" - echo "[$(date)] Total memory: ${total_mem}MB" >> "$LOG_FILE" - echo "[$(date)] Setting innodb_buffer_pool_size to $innodb_buffer_pool_size" >> "$LOG_FILE" - echo "[$(date)] Setting key_buffer_size to $key_buffer_size" >> "$LOG_FILE" - - # Create a temporary config file - cat > /etc/mysql/conf.d/memory.cnf << MYSQL_CFG -[mysqld] -innodb_buffer_pool_size = $innodb_buffer_pool_size -key_buffer_size = $key_buffer_size -MYSQL_CFG - - # Restart MySQL if running - if systemctl is-active --quiet mariadb; then - echo "[$(date)] Restarting MySQL to apply new configuration" >> "$LOG_FILE" - systemctl restart mariadb >> "$LOG_FILE" 2>&1 || echo "[$(date)] Failed to restart MySQL" >> "$LOG_FILE" - fi -} - -# Update Valkey configuration -update_valkey_config() { - local total_mem=$(get_total_memory_mb) - local maxmemory=$((total_mem * 20 / 100))mb # Using 20% of total memory for Valkey - - echo "[$(date)] Updating Valkey configuration" >> "$LOG_FILE" - echo "[$(date)] Setting maxmemory to $maxmemory" >> "$LOG_FILE" - - # Update Valkey config - if [ -f "/etc/valkey/valkey.conf" ]; then - sed -i "s/^maxmemory .*/maxmemory $maxmemory/" /etc/valkey/valkey.conf - - # Restart Valkey if running - if systemctl is-active --quiet valkey; then - echo "[$(date)] Restarting Valkey to apply new configuration" >> "$LOG_FILE" - systemctl restart valkey >> "$LOG_FILE" 2>&1 || echo "[$(date)] Failed to restart Valkey" >> "$LOG_FILE" - fi - else - echo "[$(date)] Valkey config file not found" >> "$LOG_FILE" - fi -} - -# Execute updates -update_mysql_config -update_valkey_config - -echo "[$(date)] Memory configuration update completed" >> "$LOG_FILE" -EOL - -# Make the script executable -chmod +x /usr/local/bin/update_memory_config.sh || handle_error "Failed to make memory config script executable" - - -# Add cron job to run at 3 AM daily -(crontab -l 2>/dev/null; echo "0 3 * * * /usr/local/bin/update_memory_config.sh") | crontab - || handle_error "Failed to add cron job" - -# Run the script once to apply initial configuration -/usr/local/bin/update_memory_config.sh || handle_error "Failed to run initial memory configuration" - -COMPLETED_STEPS+=("Automatic memory configuration script installed at /usr/local/bin/update_memory_config.sh") - -# Final verification -log_step "Verifying all services are running" -for service in nginx mariadb freeradius valkey-server php${PHP_VERSION}-fpm; do - if ! systemctl is-active --quiet $service; then - log_warning "$service is not running" - systemctl status $service - else - log_success "$service is running" - fi -done - -# Complete installation message -log_success "Installation completed successfully!" -echo "You can find your database credentials in $DB_CREDENTIALS_FILE" -echo "Your SimpleSpot installation is available at: https://$DOMAIN" From 009abf09e53edea3438967af7abeb030414c1b8e Mon Sep 17 00:00:00 2001 From: Barnabie Opiyo Date: Thu, 23 Jul 2026 15:32:18 +0300 Subject: [PATCH 37/45] Harden buffered accounting against reader jams (v3.3) A NAS reboot's Accounting-On record jammed the buffered-sql detail reader for ~7h on 2026-07-23: its bulk close-all-sessions query failed and the reader retried it forever, freezing accounting while the detail backlog grew to 159MB. Two defenses: - buffered-sql site (universal.sh Step 6b + both installers): if sql fails on an Accounting-On/Off record, acknowledge and drop it instead of retrying forever; session records keep retry-until-accepted. - universal.sh Step 6d: install radacct-watchdog.sh + cron.d entry that detects a detail.work mtime frozen >15m while FreeRADIUS is up and auto-applies the unjam (stop, set work file aside, start). Co-Authored-By: Claude Fable 5 --- ubuntu_simpleisp.sh | 12 +++++ ubuntu_simplespot.sh | 12 +++++ universal.sh | 117 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 141 insertions(+) diff --git a/ubuntu_simpleisp.sh b/ubuntu_simpleisp.sh index 745fd58..28a97e0 100644 --- a/ubuntu_simpleisp.sh +++ b/ubuntu_simpleisp.sh @@ -949,6 +949,18 @@ server buffered-sql { accounting { sql + + # Accounting-On/Off makes sql run a bulk close-all-sessions + # query for the NAS. If that one query fails, the reader + # retries the record forever and jams every record queued + # behind it, so acknowledge and drop just these two types. + # Session records keep retrying until SQL accepts them - + # that is the point of the buffer. + if (fail) { + if (&Acct-Status-Type == Accounting-On || &Acct-Status-Type == Accounting-Off) { + ok + } + } } } EOF diff --git a/ubuntu_simplespot.sh b/ubuntu_simplespot.sh index 3f8f6ad..65ead6f 100644 --- a/ubuntu_simplespot.sh +++ b/ubuntu_simplespot.sh @@ -946,6 +946,18 @@ server buffered-sql { accounting { sql + + # Accounting-On/Off makes sql run a bulk close-all-sessions + # query for the NAS. If that one query fails, the reader + # retries the record forever and jams every record queued + # behind it, so acknowledge and drop just these two types. + # Session records keep retrying until SQL accepts them - + # that is the point of the buffer. + if (fail) { + if (&Acct-Status-Type == Accounting-On || &Acct-Status-Type == Accounting-Off) { + ok + } + } } } EOF diff --git a/universal.sh b/universal.sh index 65d6a75..f704d7c 100644 --- a/universal.sh +++ b/universal.sh @@ -28,6 +28,13 @@ # v3.2 (2026-07-17): Step 6c appends the /usr/bin systemctl/supervisorctl # sudoers entries the panel actually invokes (plus the umbrella # "openvpn" unit); validated with visudo -c, restored on failure. +# +# v3.3 (2026-07-23): the buffered-sql site now acknowledges Accounting-On/ +# Off records whose bulk close query fails (a NAS-reboot record could +# jam the reader's retry loop and silently freeze accounting for hours); +# Step 6d installs a radacct-watchdog cron that spots a frozen +# detail.work and auto-applies the unjam (stop, set work file aside, +# start). Installers updated to write the same buffered-sql site. # --------------------------------------------------------------------------------- set -euo pipefail @@ -748,6 +755,18 @@ server buffered-sql { accounting { sql + + # Accounting-On/Off makes sql run a bulk close-all-sessions + # query for the NAS. If that one query fails, the reader + # retries the record forever and jams every record queued + # behind it, so acknowledge and drop just these two types. + # Session records keep retrying until SQL accepts them - + # that is the point of the buffer. + if (fail) { + if (&Acct-Status-Type == Accounting-On || &Acct-Status-Type == Accounting-Off) { + ok + } + } } } EOF @@ -943,6 +962,104 @@ fi log "" +# ---------------------------------------------------------------------- +# Step 6d: radacct watchdog (auto-unjam a stalled detail reader). +# A healthy buffered-sql reader either has no detail.work (idle: the +# file is deleted once fully replayed) or keeps touching it as it marks +# processed entries (track = yes). A detail.work mtime frozen while +# FreeRADIUS is up means the reader is wedged (a record SQL keeps +# rejecting, or an unclean shutdown) and accounting silently stops +# reaching the panel. The watchdog applies the manual fix: stop, set +# the work file aside for post-mortem, start. Loss is bounded to that +# file - open sessions are rebuilt by their next interim update. +# Idempotent. +# ---------------------------------------------------------------------- +log "========================" +log " Step 6d: radacct watchdog (auto-unjam stalled detail reader)" +log "------------------------" + +if [ -z "${RADIUS_CONF}" ]; then + log "[WARN] radiusd.conf not found; skipping radacct watchdog." +else + WATCHDOG_BIN="/usr/local/sbin/radacct-watchdog.sh" + WATCHDOG_CRON="/etc/cron.d/radacct-watchdog" + WATCHDOG_CHANGED=0 + + WATCHDOG_TMP="${WATCHDOG_BIN}.new" + cat > "${WATCHDOG_TMP}" << 'WDOG' +#!/bin/bash +# Installed by universal.sh (Step 6d). Unjams the FreeRADIUS +# buffered-sql detail reader: a detail.work whose mtime is frozen for +# STALE_MIN minutes while the service is up means the reader is wedged +# and no accounting is reaching SQL. Stop, set the work file aside +# (kept for post-mortem), start; the reader resumes on a fresh queue +# and open sessions reappear on their next interim update. +set -u + +WORK="/var/log/freeradius/radacct/detail.work" +WLOG="/var/log/radacct-watchdog.log" +STALE_MIN=15 + +exec 9>"/var/lock/radacct-watchdog.lock" +flock -n 9 || exit 0 + +SVC="" +for s in freeradius radiusd; do + if systemctl is-active --quiet "$s" 2>/dev/null; then SVC="$s"; break; fi +done +[ -n "$SVC" ] || exit 0 # service stopped on purpose is not a jam +[ -f "$WORK" ] || exit 0 # no work file: reader idle and healthy + +# find prints the path only when mtime is older than the cutoff +find "$WORK" -mmin "+${STALE_MIN}" 2>/dev/null | grep -q . || exit 0 + +TS="$(date +%Y%m%d_%H%M%S)" +echo "[$(date '+%F %T')] detail.work frozen >${STALE_MIN}m; unjamming (kept: ${WORK}.stuck.${TS})" >> "$WLOG" +systemctl stop "$SVC" +mv "$WORK" "${WORK}.stuck.${TS}" +systemctl start "$SVC" +echo "[$(date '+%F %T')] ${SVC} restarted; reader resumed on a fresh queue" >> "$WLOG" + +# keep a week of post-mortem files +find "$(dirname "$WORK")" -maxdepth 1 -name 'detail.work.stuck.*' -mtime +7 -delete 2>/dev/null || true +WDOG + + WATCHDOG_CRON_TMP="${WATCHDOG_CRON}.new" + cat > "${WATCHDOG_CRON_TMP}" << 'EOF' +# Installed by universal.sh (Step 6d): auto-unjam a stalled buffered-sql reader. +*/5 * * * * root /usr/local/sbin/radacct-watchdog.sh +EOF + + watchdog_install_if_changed() { + # $1 = live file, $2 = desired temp file, $3 = mode. Returns 0 if a + # change was made (or would be, under --dry-run), 1 if up to date. + local live="$1" tmp="$2" mode="$3" + if [ -f "${live}" ] && cmp -s "${live}" "${tmp}"; then + rm -f "${tmp}" || true + chmod "${mode}" "${live}" 2>/dev/null || true + return 1 + fi + if [ "$DRY_RUN" -eq 0 ]; then + mv -f "${tmp}" "${live}" + chmod "${mode}" "${live}" + log "[OK] installed ${live}" + else + rm -f "${tmp}" || true + log "[DRY] would install ${live}" + fi + return 0 + } + + if watchdog_install_if_changed "${WATCHDOG_BIN}" "${WATCHDOG_TMP}" 0755; then WATCHDOG_CHANGED=1; fi + if watchdog_install_if_changed "${WATCHDOG_CRON}" "${WATCHDOG_CRON_TMP}" 0644; then WATCHDOG_CHANGED=1; fi + + if [ "${WATCHDOG_CHANGED}" -eq 0 ]; then + log "[OK] radacct watchdog already installed; no change" + fi +fi + +log "" + # --------------------------------------------------------------------- # Step 7: PHP-FPM detection, tuning, slowlog setup, and IN-PLACE edits # --------------------------------------------------------------------- From 309bc27b4602059f321fe48f6a87602ecc3b03d2 Mon Sep 17 00:00:00 2001 From: Barnabie Opiyo Date: Thu, 23 Jul 2026 16:17:51 +0300 Subject: [PATCH 38/45] buffered-sql: fail = 1 so the Accounting-On/Off check actually runs The v3.3 tolerance never executed: a module returning fail in an accounting section returns from the section by default, so the if (fail) check after sql was dead code and a NAS reboot jammed the detail reader again with the fix deployed. Override the action with sql { fail = 1 } so processing continues into the check; ordinary records still leave the section as fail and keep being retried. Co-Authored-By: Claude Fable 5 --- ubuntu_simpleisp.sh | 7 ++++++- ubuntu_simplespot.sh | 7 ++++++- universal.sh | 12 +++++++++++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/ubuntu_simpleisp.sh b/ubuntu_simpleisp.sh index 28a97e0..0e0f3c7 100644 --- a/ubuntu_simpleisp.sh +++ b/ubuntu_simpleisp.sh @@ -948,7 +948,12 @@ server buffered-sql { } accounting { - sql + # fail = 1 overrides the default action for a failed sql + # (return), which would exit the section before the check + # below ever runs. + sql { + fail = 1 + } # Accounting-On/Off makes sql run a bulk close-all-sessions # query for the NAS. If that one query fails, the reader diff --git a/ubuntu_simplespot.sh b/ubuntu_simplespot.sh index 65ead6f..880c686 100644 --- a/ubuntu_simplespot.sh +++ b/ubuntu_simplespot.sh @@ -945,7 +945,12 @@ server buffered-sql { } accounting { - sql + # fail = 1 overrides the default action for a failed sql + # (return), which would exit the section before the check + # below ever runs. + sql { + fail = 1 + } # Accounting-On/Off makes sql run a bulk close-all-sessions # query for the NAS. If that one query fails, the reader diff --git a/universal.sh b/universal.sh index f704d7c..e184aa5 100644 --- a/universal.sh +++ b/universal.sh @@ -35,6 +35,11 @@ # Step 6d installs a radacct-watchdog cron that spots a frozen # detail.work and auto-applies the unjam (stop, set work file aside, # start). Installers updated to write the same buffered-sql site. +# +# v3.3.1 (2026-07-23): sql { fail = 1 } in the buffered-sql accounting +# section - the default action for a failed module is to return from +# the section, so the v3.3 if (fail) Accounting-On/Off check was never +# reached and a NAS reboot jammed the reader again despite the fix. # --------------------------------------------------------------------------------- set -euo pipefail @@ -754,7 +759,12 @@ server buffered-sql { } accounting { - sql + # fail = 1 overrides the default action for a failed sql + # (return), which would exit the section before the check + # below ever runs. + sql { + fail = 1 + } # Accounting-On/Off makes sql run a bulk close-all-sessions # query for the NAS. If that one query fails, the reader From ca32a2dd6dcb77340c6415accf6c3db14c438234 Mon Sep 17 00:00:00 2001 From: Barnabie Opiyo Date: Thu, 23 Jul 2026 16:24:05 +0300 Subject: [PATCH 39/45] radacct-watchdog: react in ~5 min, but only while MariaDB is up STALE_MIN 15->5 and the cron entry runs every minute, cutting wedge detection from 15-20 minutes to ~5-6. To keep the shorter fuse from discarding legitimately buffered records, the watchdog now exits without acting when mysqladmin ping fails: a frozen reader during a DB outage is the buffer working as designed. Co-Authored-By: Claude Fable 5 --- universal.sh | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/universal.sh b/universal.sh index e184aa5..9c15492 100644 --- a/universal.sh +++ b/universal.sh @@ -40,6 +40,11 @@ # section - the default action for a failed module is to return from # the section, so the v3.3 if (fail) Accounting-On/Off check was never # reached and a NAS reboot jammed the reader again despite the fix. +# +# v3.3.2 (2026-07-23): faster watchdog - STALE_MIN 15->5 and cron every +# minute (detects a wedge in ~5-6 min instead of 15-20), gated on +# mysqladmin ping so a genuine MariaDB outage keeps buffering instead +# of having its work file set aside. # --------------------------------------------------------------------------------- set -euo pipefail @@ -1008,7 +1013,7 @@ set -u WORK="/var/log/freeradius/radacct/detail.work" WLOG="/var/log/radacct-watchdog.log" -STALE_MIN=15 +STALE_MIN=5 exec 9>"/var/lock/radacct-watchdog.lock" flock -n 9 || exit 0 @@ -1023,6 +1028,13 @@ done # find prints the path only when mtime is older than the cutoff find "$WORK" -mmin "+${STALE_MIN}" 2>/dev/null | grep -q . || exit 0 +# Only unjam while MariaDB answers: a frozen reader during a DB outage +# is the buffer doing its job (records replay once the DB returns), and +# setting the work file aside then would discard them for nothing. +if command -v mysqladmin >/dev/null 2>&1; then + mysqladmin --connect-timeout=5 ping >/dev/null 2>&1 || exit 0 +fi + TS="$(date +%Y%m%d_%H%M%S)" echo "[$(date '+%F %T')] detail.work frozen >${STALE_MIN}m; unjamming (kept: ${WORK}.stuck.${TS})" >> "$WLOG" systemctl stop "$SVC" @@ -1037,7 +1049,7 @@ WDOG WATCHDOG_CRON_TMP="${WATCHDOG_CRON}.new" cat > "${WATCHDOG_CRON_TMP}" << 'EOF' # Installed by universal.sh (Step 6d): auto-unjam a stalled buffered-sql reader. -*/5 * * * * root /usr/local/sbin/radacct-watchdog.sh +* * * * * root /usr/local/sbin/radacct-watchdog.sh EOF watchdog_install_if_changed() { From fe5813adfd700e95b2f0d1c76d40bdfcdf8f8eb2 Mon Sep 17 00:00:00 2001 From: Barnabie Opiyo Date: Thu, 23 Jul 2026 16:31:25 +0300 Subject: [PATCH 40/45] Keep Accounting-On/Off out of the buffered queue (v3.4) Root cause of today's repeated jams was not a failing SQL query: the detail reader thread dies silently when it processes a NAS-reboot record. Evidence: frozen detail.work with the Accounting-On at its head (three for three today), all FreeRADIUS DB connections idle during the freeze, nothing in radius.log until "Reader thread exited without informing the master: Bad file descriptor" at the next stop. The v3.3.x unlang tolerance sat inside the reader's virtual server, so it could never fire. The default site now handles Accounting-On/Off synchronously via sql (fail = 1 + ok so the NAS is always acked; the bulk stale-session close is best-effort) and queues only session records to detail. Both default-site rewriters count braces (comments stripped) instead of ending at the first "}", keeping regeneration idempotent now that the emitted section nests. Verified: stock and flat inputs convert correctly and re-running on the output is byte-stable. Co-Authored-By: Claude Fable 5 --- ubuntu_simpleisp.sh | 30 ++++++++++++++++++++++++------ ubuntu_simplespot.sh | 30 ++++++++++++++++++++++++------ universal.sh | 43 +++++++++++++++++++++++++++++++++++++------ 3 files changed, 85 insertions(+), 18 deletions(-) diff --git a/ubuntu_simpleisp.sh b/ubuntu_simpleisp.sh index 0e0f3c7..e1ae161 100644 --- a/ubuntu_simpleisp.sh +++ b/ubuntu_simpleisp.sh @@ -1059,20 +1059,38 @@ fi if [ -f "$DEFAULT_SITE_AVAIL" ]; then sed -i 's/-sql/sql/g' "$DEFAULT_SITE_AVAIL" || handle_error "Failed to update -sql to sql" log_step "Replacing accounting section in FreeRADIUS default site" - # Accounting writes to the local detail file only; the buffered-sql - # virtual server replays it into SQL (survives DB stalls/restarts). + # Session records queue to the local detail file only; the buffered-sql + # virtual server replays them into SQL (survives DB stalls/restarts). + # Accounting-On/Off go to sql synchronously instead: NAS-reboot records + # crash the detail reader thread and freeze the queue, so they must + # never enter it. cat << 'EOF' > /tmp/new_accounting_block accounting { +if (&Acct-Status-Type == Accounting-On || &Acct-Status-Type == Accounting-Off) { +sql { +fail = 1 +} +ok +} +else { detail +} exec attr_filter.accounting_response } EOF + # Brace-counting skip (comments stripped): the replaced section can be + # the nested block above on re-runs, where "end at first }" corrupts it. awk ' - BEGIN { skip = 0 } - /^accounting[ \t]*{/ { print_block = 1; print_file("/tmp/new_accounting_block"); skip = 1; next } - /^[ \t]*}/ { if (skip) { skip = 0; next } } - !skip { print } + BEGIN { skip = 0; depth = 0 } + /^accounting[ \t]*{/ { print_file("/tmp/new_accounting_block"); skip = 1; depth = 1; next } + skip { + s = $0; sub(/#.*/, "", s) + depth += gsub(/{/, "{", s) - gsub(/}/, "}", s) + if (depth <= 0) skip = 0 + next + } + { print } function print_file(file) { while ((getline line < file) > 0) print line; close(file) diff --git a/ubuntu_simplespot.sh b/ubuntu_simplespot.sh index 880c686..8f8548c 100644 --- a/ubuntu_simplespot.sh +++ b/ubuntu_simplespot.sh @@ -1056,20 +1056,38 @@ fi if [ -f "$DEFAULT_SITE_AVAIL" ]; then sed -i 's/-sql/sql/g' "$DEFAULT_SITE_AVAIL" || handle_error "Failed to update -sql to sql" log_step "Replacing accounting section in FreeRADIUS default site" - # Accounting writes to the local detail file only; the buffered-sql - # virtual server replays it into SQL (survives DB stalls/restarts). + # Session records queue to the local detail file only; the buffered-sql + # virtual server replays them into SQL (survives DB stalls/restarts). + # Accounting-On/Off go to sql synchronously instead: NAS-reboot records + # crash the detail reader thread and freeze the queue, so they must + # never enter it. cat << 'EOF' > /tmp/new_accounting_block accounting { +if (&Acct-Status-Type == Accounting-On || &Acct-Status-Type == Accounting-Off) { +sql { +fail = 1 +} +ok +} +else { detail +} exec attr_filter.accounting_response } EOF + # Brace-counting skip (comments stripped): the replaced section can be + # the nested block above on re-runs, where "end at first }" corrupts it. awk ' - BEGIN { skip = 0 } - /^accounting[ \t]*{/ { print_block = 1; print_file("/tmp/new_accounting_block"); skip = 1; next } - /^[ \t]*}/ { if (skip) { skip = 0; next } } - !skip { print } + BEGIN { skip = 0; depth = 0 } + /^accounting[ \t]*{/ { print_file("/tmp/new_accounting_block"); skip = 1; depth = 1; next } + skip { + s = $0; sub(/#.*/, "", s) + depth += gsub(/{/, "{", s) - gsub(/}/, "}", s) + if (depth <= 0) skip = 0 + next + } + { print } function print_file(file) { while ((getline line < file) > 0) print line; close(file) diff --git a/universal.sh b/universal.sh index 9c15492..32ef749 100644 --- a/universal.sh +++ b/universal.sh @@ -45,6 +45,16 @@ # minute (detects a wedge in ~5-6 min instead of 15-20), gated on # mysqladmin ping so a genuine MariaDB outage keeps buffering instead # of having its work file set aside. +# +# v3.4 (2026-07-23): Accounting-On/Off never enters the buffered queue. +# Root cause of the day's jams: the detail reader thread dies silently +# on NAS-reboot records (frozen detail.work, idle SQL connections, no +# error until "Reader thread exited without informing the master" at +# the next stop) - not a failing SQL query, so the v3.3.x unlang +# tolerance could never fire. The default site now runs sql +# synchronously for On/Off (fail-tolerant, always acked) and queues +# only session records to detail. Both site rewriters are now +# brace-aware so regenerating the nested section stays idempotent. # --------------------------------------------------------------------------------- set -euo pipefail @@ -787,8 +797,13 @@ server buffered-sql { EOF # Desired default site, matching the installers exactly: -sql refs - # enabled and the accounting section writing to detail only (SQL happens - # in buffered-sql). Generated from the currently EFFECTIVE config - + # enabled and the accounting section queueing session records to detail + # only (SQL happens in buffered-sql) - EXCEPT Accounting-On/Off, which + # are handled synchronously via sql. NAS-reboot records crash the + # detail reader thread (frozen detail.work, idle SQL connections, + # "Reader thread exited without informing the master" on the next + # stop), so they must never enter the buffered queue. + # Generated from the currently EFFECTIVE config - # sites-enabled/default if present (older installers left an edited # regular file there), else sites-available/default. Regeneration is # stable, so cmp below makes this a no-op on already-converted servers. @@ -805,18 +820,34 @@ EOF fi if [ -n "${RAD_DEFAULT_SRC}" ]; then RAD_DEFAULT_TMP="${RAD_DEFAULT_AVAIL}.new" + # The skip logic counts braces (comments stripped) because the + # generated section is nested; ending at the first "}" would leave + # fragments of the old section behind on re-runs. sed 's/-sql/sql/g' "${RAD_DEFAULT_SRC}" | awk ' - BEGIN { skip = 0 } + BEGIN { skip = 0; depth = 0 } /^accounting[ \t]*{/ { print "accounting {" + print "if (&Acct-Status-Type == Accounting-On || &Acct-Status-Type == Accounting-Off) {" + print "sql {" + print "fail = 1" + print "}" + print "ok" + print "}" + print "else {" print "detail" + print "}" print "exec" print "attr_filter.accounting_response" print "}" - skip = 1; next + skip = 1; depth = 1; next + } + skip { + s = $0; sub(/#.*/, "", s) + depth += gsub(/{/, "{", s) - gsub(/}/, "}", s) + if (depth <= 0) skip = 0 + next } - /^[ \t]*}/ { if (skip) { skip = 0; next } } - !skip { print } + { print } ' > "${RAD_DEFAULT_TMP}" || true fi From 07be95ff71ca5e84d4fc029ba9a7d8887d2b1ceb Mon Sep 17 00:00:00 2001 From: Barnabie Opiyo Date: Thu, 23 Jul 2026 18:24:03 +0300 Subject: [PATCH 41/45] Retire buffered accounting - direct sql in the default site (v3.5) Decision after a day of jams: the buffer protected little (auth already needs the DB synchronously, and 1-minute cumulative interims rebuild counters within a minute of any DB blip) while its real-world failure mode - the detail reader thread dying silently on NAS-reboot records - froze accounting until a human or watchdog intervened. universal.sh Step 6b now converges the default site to accounting { sql } and disables the buffered-sql site and detail module links (available files kept for reference; symlinks restored on failed -XC validation). Step 6d removes the radacct watchdog it previously installed - there is no detail.work left to unjam. Installers no longer create the buffered-sql site or detail module. Rewriter verified: v3.4 nested and stock flat sections both convert cleanly and re-runs are byte-stable. Co-Authored-By: Claude Fable 5 --- ubuntu_simpleisp.sh | 82 ++------------ ubuntu_simplespot.sh | 82 ++------------ universal.sh | 256 +++++++++++-------------------------------- 3 files changed, 86 insertions(+), 334 deletions(-) diff --git a/ubuntu_simpleisp.sh b/ubuntu_simpleisp.sh index e1ae161..0744fe9 100644 --- a/ubuntu_simpleisp.sh +++ b/ubuntu_simpleisp.sh @@ -929,62 +929,8 @@ if [ -f "${FREERADIUS_CONF_DIR}/mods-available/sql" ]; then fi COMPLETED_STEPS+=("Completed checking FreeRADIUS files") -# Write and enable the buffered-sql site. Accounting packets are written to -# a local detail file by the default site (fast, survives DB stalls); this -# virtual server tails that file and replays the records into SQL. -log_step "Enabling FreeRADIUS buffered-sql site" -mkdir -p "${FREERADIUS_CONF_DIR}/sites-available" "${FREERADIUS_CONF_DIR}/sites-enabled" || handle_error "Failed to create FreeRADIUS sites directories" -cat > "${FREERADIUS_CONF_DIR}/sites-available/buffered-sql" << 'EOF' -server buffered-sql { - listen { - type = detail - filename = "${radacctdir}/detail" - load_factor = 10 - track = yes - } - - preacct { - preprocess - } - - accounting { - # fail = 1 overrides the default action for a failed sql - # (return), which would exit the section before the check - # below ever runs. - sql { - fail = 1 - } - - # Accounting-On/Off makes sql run a bulk close-all-sessions - # query for the NAS. If that one query fails, the reader - # retries the record forever and jams every record queued - # behind it, so acknowledge and drop just these two types. - # Session records keep retrying until SQL accepts them - - # that is the point of the buffer. - if (fail) { - if (&Acct-Status-Type == Accounting-On || &Acct-Status-Type == Accounting-Off) { - ok - } - } - } -} -EOF -ln -sf "${FREERADIUS_CONF_DIR}/sites-available/buffered-sql" "${FREERADIUS_CONF_DIR}/sites-enabled/buffered-sql" || handle_error "Failed to enable buffered-sql site" -COMPLETED_STEPS+=("FreeRADIUS buffered-sql site enabled") - -# Configure the detail module as a single-file writer that buffered-sql can -# consume (the stock module writes per-NAS/per-day files the reader ignores). -log_step "Configuring FreeRADIUS detail module" -cat > "${FREERADIUS_CONF_DIR}/mods-available/detail" << 'EOF' -detail { - filename = ${radacctdir}/detail - header = "%t" - permissions = 0600 - locking = yes -} -EOF -ln -sf "${FREERADIUS_CONF_DIR}/mods-available/detail" "${FREERADIUS_CONF_DIR}/mods-enabled/detail" || handle_error "Failed to enable detail module" -COMPLETED_STEPS+=("FreeRADIUS detail module configured") +# NOTE: buffered accounting (detail -> buffered-sql) was retired +# 2026-07-23; accounting now goes straight to sql in the default site. # Enable SQL module for FreeRADIUS log_step "Enabling SQL module" @@ -1059,28 +1005,20 @@ fi if [ -f "$DEFAULT_SITE_AVAIL" ]; then sed -i 's/-sql/sql/g' "$DEFAULT_SITE_AVAIL" || handle_error "Failed to update -sql to sql" log_step "Replacing accounting section in FreeRADIUS default site" - # Session records queue to the local detail file only; the buffered-sql - # virtual server replays them into SQL (survives DB stalls/restarts). - # Accounting-On/Off go to sql synchronously instead: NAS-reboot records - # crash the detail reader thread and freeze the queue, so they must - # never enter it. + # Accounting goes straight to sql. Buffered accounting (detail -> + # buffered-sql) was retired 2026-07-23: the detail reader thread dies + # silently on NAS-reboot records, and with 1-minute cumulative + # interims direct sql self-heals within a minute of any DB blip. cat << 'EOF' > /tmp/new_accounting_block accounting { -if (&Acct-Status-Type == Accounting-On || &Acct-Status-Type == Accounting-Off) { -sql { -fail = 1 -} -ok -} -else { -detail -} +sql exec attr_filter.accounting_response } EOF - # Brace-counting skip (comments stripped): the replaced section can be - # the nested block above on re-runs, where "end at first }" corrupts it. + # Brace-counting skip (comments stripped): the replaced section may be + # the nested block older buffered builds generated, where "end at + # first }" corrupts it. awk ' BEGIN { skip = 0; depth = 0 } /^accounting[ \t]*{/ { print_file("/tmp/new_accounting_block"); skip = 1; depth = 1; next } diff --git a/ubuntu_simplespot.sh b/ubuntu_simplespot.sh index 8f8548c..9ccec8d 100644 --- a/ubuntu_simplespot.sh +++ b/ubuntu_simplespot.sh @@ -926,62 +926,8 @@ if [ -f "${FREERADIUS_CONF_DIR}/mods-available/sql" ]; then fi COMPLETED_STEPS+=("Completed checking FreeRADIUS files") -# Write and enable the buffered-sql site. Accounting packets are written to -# a local detail file by the default site (fast, survives DB stalls); this -# virtual server tails that file and replays the records into SQL. -log_step "Enabling FreeRADIUS buffered-sql site" -mkdir -p "${FREERADIUS_CONF_DIR}/sites-available" "${FREERADIUS_CONF_DIR}/sites-enabled" || handle_error "Failed to create FreeRADIUS sites directories" -cat > "${FREERADIUS_CONF_DIR}/sites-available/buffered-sql" << 'EOF' -server buffered-sql { - listen { - type = detail - filename = "${radacctdir}/detail" - load_factor = 10 - track = yes - } - - preacct { - preprocess - } - - accounting { - # fail = 1 overrides the default action for a failed sql - # (return), which would exit the section before the check - # below ever runs. - sql { - fail = 1 - } - - # Accounting-On/Off makes sql run a bulk close-all-sessions - # query for the NAS. If that one query fails, the reader - # retries the record forever and jams every record queued - # behind it, so acknowledge and drop just these two types. - # Session records keep retrying until SQL accepts them - - # that is the point of the buffer. - if (fail) { - if (&Acct-Status-Type == Accounting-On || &Acct-Status-Type == Accounting-Off) { - ok - } - } - } -} -EOF -ln -sf "${FREERADIUS_CONF_DIR}/sites-available/buffered-sql" "${FREERADIUS_CONF_DIR}/sites-enabled/buffered-sql" || handle_error "Failed to enable buffered-sql site" -COMPLETED_STEPS+=("FreeRADIUS buffered-sql site enabled") - -# Configure the detail module as a single-file writer that buffered-sql can -# consume (the stock module writes per-NAS/per-day files the reader ignores). -log_step "Configuring FreeRADIUS detail module" -cat > "${FREERADIUS_CONF_DIR}/mods-available/detail" << 'EOF' -detail { - filename = ${radacctdir}/detail - header = "%t" - permissions = 0600 - locking = yes -} -EOF -ln -sf "${FREERADIUS_CONF_DIR}/mods-available/detail" "${FREERADIUS_CONF_DIR}/mods-enabled/detail" || handle_error "Failed to enable detail module" -COMPLETED_STEPS+=("FreeRADIUS detail module configured") +# NOTE: buffered accounting (detail -> buffered-sql) was retired +# 2026-07-23; accounting now goes straight to sql in the default site. # Enable SQL module for FreeRADIUS log_step "Enabling SQL module" @@ -1056,28 +1002,20 @@ fi if [ -f "$DEFAULT_SITE_AVAIL" ]; then sed -i 's/-sql/sql/g' "$DEFAULT_SITE_AVAIL" || handle_error "Failed to update -sql to sql" log_step "Replacing accounting section in FreeRADIUS default site" - # Session records queue to the local detail file only; the buffered-sql - # virtual server replays them into SQL (survives DB stalls/restarts). - # Accounting-On/Off go to sql synchronously instead: NAS-reboot records - # crash the detail reader thread and freeze the queue, so they must - # never enter it. + # Accounting goes straight to sql. Buffered accounting (detail -> + # buffered-sql) was retired 2026-07-23: the detail reader thread dies + # silently on NAS-reboot records, and with 1-minute cumulative + # interims direct sql self-heals within a minute of any DB blip. cat << 'EOF' > /tmp/new_accounting_block accounting { -if (&Acct-Status-Type == Accounting-On || &Acct-Status-Type == Accounting-Off) { -sql { -fail = 1 -} -ok -} -else { -detail -} +sql exec attr_filter.accounting_response } EOF - # Brace-counting skip (comments stripped): the replaced section can be - # the nested block above on re-runs, where "end at first }" corrupts it. + # Brace-counting skip (comments stripped): the replaced section may be + # the nested block older buffered builds generated, where "end at + # first }" corrupts it. awk ' BEGIN { skip = 0; depth = 0 } /^accounting[ \t]*{/ { print_file("/tmp/new_accounting_block"); skip = 1; depth = 1; next } diff --git a/universal.sh b/universal.sh index 32ef749..d1b77b5 100644 --- a/universal.sh +++ b/universal.sh @@ -55,6 +55,15 @@ # synchronously for On/Off (fail-tolerant, always acked) and queues # only session records to detail. Both site rewriters are now # brace-aware so regenerating the nested section stays idempotent. +# +# v3.5 (2026-07-23): buffered accounting RETIRED - back to direct sql +# accounting in the default site. Auth already requires the DB +# synchronously (a DB outage stops logins regardless), 1-minute +# cumulative interims rebuild counters within a minute of any DB +# blip, and the buffer's real-world failure mode (reader thread dying +# on NAS-reboot records) is silent and needs babysitting. Step 6b now +# converges to accounting { sql } and disables the buffered-sql site +# + detail module links; Step 6d removes the watchdog it installed. # --------------------------------------------------------------------------------- set -euo pipefail @@ -726,83 +735,32 @@ fi log "" # ---------------------------------------------------------------------- -# Step 6b: Ensure buffered accounting wiring (detail -> buffered-sql). -# The default site writes accounting to a local detail file and the -# buffered-sql virtual server replays it into SQL, so records survive -# MariaDB stalls/restarts. Mirrors the installers; idempotent, so -# already-wired servers see no change and no restart. +# Step 6b: Ensure DIRECT accounting wiring (accounting -> sql). +# Buffered accounting (detail file -> buffered-sql reader) was retired +# 2026-07-23: the reader thread dies silently on NAS-reboot records, +# freezing accounting until someone intervenes. Auth already needs the +# DB synchronously (a DB outage stops logins regardless), and 1-minute +# cumulative interims rebuild counters within a minute of a DB blip, +# so the buffer added a silent failure mode without real protection. +# Converges the default site to plain sql accounting and disables the +# buffered-sql site / detail module links. Idempotent: converged +# servers see no change and no restart. # ---------------------------------------------------------------------- log "========================" -log " Step 6b: Buffered accounting wiring (detail -> buffered-sql)" +log " Step 6b: Direct accounting wiring (accounting -> sql)" log "------------------------" if [ -z "${RADIUS_CONF}" ]; then - log "[WARN] radiusd.conf not found; skipping buffered accounting wiring." + log "[WARN] radiusd.conf not found; skipping accounting wiring." else RAD_ROOT="$(dirname "${RADIUS_CONF}")" BUFSQL_CHANGED=0 - BUFSQL_TOUCHED=() # files backed up this run, restored on failed validation - BUFSQL_NEW_LINKS=() # symlinks created this run, removed on failed validation - - # Desired detail module: a single-file writer buffered-sql can consume - # (stock writes per-NAS/per-day files the reader ignores). - DETAIL_MOD="${RAD_ROOT}/mods-available/detail" - DETAIL_TMP="${DETAIL_MOD}.new" - cat > "${DETAIL_TMP}" << 'EOF' -detail { - filename = ${radacctdir}/detail - header = "%t" - permissions = 0600 - locking = yes -} -EOF - - # Desired buffered-sql site: tail the detail file, replay into SQL. - BUFSQL_SITE="${RAD_ROOT}/sites-available/buffered-sql" - BUFSQL_TMP="${BUFSQL_SITE}.new" - cat > "${BUFSQL_TMP}" << 'EOF' -server buffered-sql { - listen { - type = detail - filename = "${radacctdir}/detail" - load_factor = 10 - track = yes - } - - preacct { - preprocess - } - - accounting { - # fail = 1 overrides the default action for a failed sql - # (return), which would exit the section before the check - # below ever runs. - sql { - fail = 1 - } - - # Accounting-On/Off makes sql run a bulk close-all-sessions - # query for the NAS. If that one query fails, the reader - # retries the record forever and jams every record queued - # behind it, so acknowledge and drop just these two types. - # Session records keep retrying until SQL accepts them - - # that is the point of the buffer. - if (fail) { - if (&Acct-Status-Type == Accounting-On || &Acct-Status-Type == Accounting-Off) { - ok - } - } - } -} -EOF + BUFSQL_TOUCHED=() # files backed up this run, restored on failed validation + BUFSQL_NEW_LINKS=() # symlinks created this run, removed on failed validation + BUFSQL_REMOVED_LINKS=() # "link:target" removed this run, re-created on failed validation # Desired default site, matching the installers exactly: -sql refs - # enabled and the accounting section queueing session records to detail - # only (SQL happens in buffered-sql) - EXCEPT Accounting-On/Off, which - # are handled synchronously via sql. NAS-reboot records crash the - # detail reader thread (frozen detail.work, idle SQL connections, - # "Reader thread exited without informing the master" on the next - # stop), so they must never enter the buffered queue. + # enabled and the accounting section writing straight to sql. # Generated from the currently EFFECTIVE config - # sites-enabled/default if present (older installers left an edited # regular file there), else sites-available/default. Regeneration is @@ -821,21 +779,13 @@ EOF if [ -n "${RAD_DEFAULT_SRC}" ]; then RAD_DEFAULT_TMP="${RAD_DEFAULT_AVAIL}.new" # The skip logic counts braces (comments stripped) because the - # generated section is nested; ending at the first "}" would leave - # fragments of the old section behind on re-runs. + # section being replaced may be the nested block earlier versions + # generated; ending at the first "}" would leave fragments behind. sed 's/-sql/sql/g' "${RAD_DEFAULT_SRC}" | awk ' BEGIN { skip = 0; depth = 0 } /^accounting[ \t]*{/ { print "accounting {" - print "if (&Acct-Status-Type == Accounting-On || &Acct-Status-Type == Accounting-Off) {" - print "sql {" - print "fail = 1" - print "}" - print "ok" - print "}" - print "else {" - print "detail" - print "}" + print "sql" print "exec" print "attr_filter.accounting_response" print "}" @@ -874,8 +824,6 @@ EOF return 0 } - if bufsql_install_if_changed "${DETAIL_MOD}" "${DETAIL_TMP}"; then BUFSQL_CHANGED=1; fi - if bufsql_install_if_changed "${BUFSQL_SITE}" "${BUFSQL_TMP}"; then BUFSQL_CHANGED=1; fi if [ -n "${RAD_DEFAULT_TMP}" ]; then if bufsql_install_if_changed "${RAD_DEFAULT_AVAIL}" "${RAD_DEFAULT_TMP}"; then BUFSQL_CHANGED=1; fi # Normalize sites-enabled/default back to the packaged symlink layout: @@ -900,44 +848,48 @@ EOF fi fi - # Enable the detail module and buffered-sql site if not already enabled. - for pair in "mods-available/detail:mods-enabled/detail" "sites-available/buffered-sql:sites-enabled/buffered-sql"; do - BUFSQL_SRC="${RAD_ROOT}/${pair%%:*}" - BUFSQL_DST="${RAD_ROOT}/${pair##*:}" - if [ ! -e "${BUFSQL_DST}" ]; then + # Disable the buffered-sql site and detail module if still enabled. + # sites-available/buffered-sql and mods-available/detail are left in + # place (inert without their links) for reference and easy rollback. + for name in "sites-enabled/buffered-sql" "mods-enabled/detail"; do + BUFSQL_DST="${RAD_ROOT}/${name}" + if [ -e "${BUFSQL_DST}" ] || [ -L "${BUFSQL_DST}" ]; then if [ "$DRY_RUN" -eq 0 ]; then - ln -sf "${BUFSQL_SRC}" "${BUFSQL_DST}" - BUFSQL_NEW_LINKS+=("${BUFSQL_DST}") - log "[OK] enabled ${BUFSQL_DST}" + if [ -L "${BUFSQL_DST}" ]; then + BUFSQL_REMOVED_LINKS+=("${BUFSQL_DST}:$(readlink "${BUFSQL_DST}")") + rm -f "${BUFSQL_DST}" + else + # Older installs sometimes left a regular file here. + cp -a "${BUFSQL_DST}" "${BUFSQL_DST}.bak.${TIMESTAMP}" + prune_baks "${BUFSQL_DST}" + BUFSQL_TOUCHED+=("${BUFSQL_DST}") + rm -f "${BUFSQL_DST}" + fi + log "[OK] disabled ${BUFSQL_DST}" else - log "[DRY] would enable ${BUFSQL_DST}" + log "[DRY] would disable ${BUFSQL_DST}" fi BUFSQL_CHANGED=1 fi done if [ "${BUFSQL_CHANGED}" -eq 0 ]; then - log "[OK] buffered accounting already wired; no change, no restart" + log "[OK] direct accounting already wired; no change, no restart" elif [ "$DRY_RUN" -eq 0 ]; then - # Queue directory must exist before validation/first write. - mkdir -p /var/log/freeradius/radacct 2>/dev/null || true - if id freerad >/dev/null 2>&1; then - chown freerad:freerad /var/log/freeradius/radacct 2>/dev/null || true - fi - # Validate BEFORE the restart at the end of the run; roll everything # back on failure so a broken config never reaches the service. RAD_BIN="$(command -v freeradius || command -v radiusd || true)" if [ -n "${RAD_BIN}" ] && ! "${RAD_BIN}" -XC >/dev/null 2>&1; then - log "[ERROR] buffered accounting config FAILED validation (${RAD_BIN} -XC); rolling back" + log "[ERROR] direct accounting config FAILED validation (${RAD_BIN} -XC); rolling back" for f in "${BUFSQL_TOUCHED[@]}"; do # rm first: if the live path became a symlink this run, cp -a onto # it would write through the link instead of replacing it. if [ -f "${f}.bak.${TIMESTAMP}" ]; then rm -f "${f}"; cp -a "${f}.bak.${TIMESTAMP}" "${f}"; fi done for l in "${BUFSQL_NEW_LINKS[@]}"; do rm -f "${l}" || true; done + for l in "${BUFSQL_REMOVED_LINKS[@]}"; do ln -sf "${l#*:}" "${l%%:*}" || true; done else - log "[OK] buffered accounting wiring applied and validated" + log "[OK] direct accounting wiring applied and validated" RADIUS_CHANGED=1 # reuse the existing end-of-run FreeRADIUS restart fi else @@ -1009,106 +961,30 @@ fi log "" # ---------------------------------------------------------------------- -# Step 6d: radacct watchdog (auto-unjam a stalled detail reader). -# A healthy buffered-sql reader either has no detail.work (idle: the -# file is deleted once fully replayed) or keeps touching it as it marks -# processed entries (track = yes). A detail.work mtime frozen while -# FreeRADIUS is up means the reader is wedged (a record SQL keeps -# rejecting, or an unclean shutdown) and accounting silently stops -# reaching the panel. The watchdog applies the manual fix: stop, set -# the work file aside for post-mortem, start. Loss is bounded to that -# file - open sessions are rebuilt by their next interim update. -# Idempotent. +# Step 6d: Remove the radacct watchdog (retired with buffered-sql). +# Direct sql accounting has no detail.work to unjam, so drop the cron +# entry and helper script from servers that received them. The log at +# /var/log/radacct-watchdog.log and any detail.work.stuck.* post-mortem +# files are left in place as history. Idempotent. # ---------------------------------------------------------------------- log "========================" -log " Step 6d: radacct watchdog (auto-unjam stalled detail reader)" +log " Step 6d: Remove radacct watchdog (obsolete with direct accounting)" log "------------------------" -if [ -z "${RADIUS_CONF}" ]; then - log "[WARN] radiusd.conf not found; skipping radacct watchdog." -else - WATCHDOG_BIN="/usr/local/sbin/radacct-watchdog.sh" - WATCHDOG_CRON="/etc/cron.d/radacct-watchdog" - WATCHDOG_CHANGED=0 - - WATCHDOG_TMP="${WATCHDOG_BIN}.new" - cat > "${WATCHDOG_TMP}" << 'WDOG' -#!/bin/bash -# Installed by universal.sh (Step 6d). Unjams the FreeRADIUS -# buffered-sql detail reader: a detail.work whose mtime is frozen for -# STALE_MIN minutes while the service is up means the reader is wedged -# and no accounting is reaching SQL. Stop, set the work file aside -# (kept for post-mortem), start; the reader resumes on a fresh queue -# and open sessions reappear on their next interim update. -set -u - -WORK="/var/log/freeradius/radacct/detail.work" -WLOG="/var/log/radacct-watchdog.log" -STALE_MIN=5 - -exec 9>"/var/lock/radacct-watchdog.lock" -flock -n 9 || exit 0 - -SVC="" -for s in freeradius radiusd; do - if systemctl is-active --quiet "$s" 2>/dev/null; then SVC="$s"; break; fi -done -[ -n "$SVC" ] || exit 0 # service stopped on purpose is not a jam -[ -f "$WORK" ] || exit 0 # no work file: reader idle and healthy - -# find prints the path only when mtime is older than the cutoff -find "$WORK" -mmin "+${STALE_MIN}" 2>/dev/null | grep -q . || exit 0 - -# Only unjam while MariaDB answers: a frozen reader during a DB outage -# is the buffer doing its job (records replay once the DB returns), and -# setting the work file aside then would discard them for nothing. -if command -v mysqladmin >/dev/null 2>&1; then - mysqladmin --connect-timeout=5 ping >/dev/null 2>&1 || exit 0 -fi - -TS="$(date +%Y%m%d_%H%M%S)" -echo "[$(date '+%F %T')] detail.work frozen >${STALE_MIN}m; unjamming (kept: ${WORK}.stuck.${TS})" >> "$WLOG" -systemctl stop "$SVC" -mv "$WORK" "${WORK}.stuck.${TS}" -systemctl start "$SVC" -echo "[$(date '+%F %T')] ${SVC} restarted; reader resumed on a fresh queue" >> "$WLOG" - -# keep a week of post-mortem files -find "$(dirname "$WORK")" -maxdepth 1 -name 'detail.work.stuck.*' -mtime +7 -delete 2>/dev/null || true -WDOG - - WATCHDOG_CRON_TMP="${WATCHDOG_CRON}.new" - cat > "${WATCHDOG_CRON_TMP}" << 'EOF' -# Installed by universal.sh (Step 6d): auto-unjam a stalled buffered-sql reader. -* * * * * root /usr/local/sbin/radacct-watchdog.sh -EOF - - watchdog_install_if_changed() { - # $1 = live file, $2 = desired temp file, $3 = mode. Returns 0 if a - # change was made (or would be, under --dry-run), 1 if up to date. - local live="$1" tmp="$2" mode="$3" - if [ -f "${live}" ] && cmp -s "${live}" "${tmp}"; then - rm -f "${tmp}" || true - chmod "${mode}" "${live}" 2>/dev/null || true - return 1 - fi +WATCHDOG_REMOVED=0 +for f in /etc/cron.d/radacct-watchdog /usr/local/sbin/radacct-watchdog.sh; do + if [ -e "$f" ]; then if [ "$DRY_RUN" -eq 0 ]; then - mv -f "${tmp}" "${live}" - chmod "${mode}" "${live}" - log "[OK] installed ${live}" + rm -f "$f" + log "[OK] removed $f" else - rm -f "${tmp}" || true - log "[DRY] would install ${live}" + log "[DRY] would remove $f" fi - return 0 - } - - if watchdog_install_if_changed "${WATCHDOG_BIN}" "${WATCHDOG_TMP}" 0755; then WATCHDOG_CHANGED=1; fi - if watchdog_install_if_changed "${WATCHDOG_CRON}" "${WATCHDOG_CRON_TMP}" 0644; then WATCHDOG_CHANGED=1; fi - - if [ "${WATCHDOG_CHANGED}" -eq 0 ]; then - log "[OK] radacct watchdog already installed; no change" + WATCHDOG_REMOVED=1 fi +done +if [ "${WATCHDOG_REMOVED}" -eq 0 ]; then + log "[OK] radacct watchdog not present; nothing to remove" fi log "" From 9f4deb58973167ebd975a340d6c98e626a382778 Mon Sep 17 00:00:00 2001 From: Barnabie Opiyo Date: Thu, 23 Jul 2026 18:41:07 +0300 Subject: [PATCH 42/45] Step 6e: radacct reconcile cron scaled to the 1-minute interim cadence With interim-update=1m, acctupdatetime freshness IS liveness. Every minute: close open rows silent 5+ minutes (lost Stop), reopen closed rows still receiving interims (live sessions caught by a sweep or bulk close - interims update closed rows but nothing else reopens them). The reopen requires updates fresher than 5 minutes so backdated or replayed timestamps cannot resurrect dead rows - the trap today's manual reopen fell into. The pair is self-correcting across server outages: silence closes, the first interim after recovery reopens. Adds idx_radacct_stop_update (acctstoptime, acctupdatetime) to the ensure list so both statements stay index-covered. Co-Authored-By: Claude Fable 5 --- universal.sh | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/universal.sh b/universal.sh index d1b77b5..b969343 100644 --- a/universal.sh +++ b/universal.sh @@ -64,6 +64,13 @@ # on NAS-reboot records) is silent and needs babysitting. Step 6b now # converges to accounting { sql } and disables the buffered-sql site # + detail module links; Step 6d removes the watchdog it installed. +# +# v3.5.1 (2026-07-23): Step 6e installs a radacct-reconcile cron with +# thresholds scaled to the 1-minute interim cadence: every minute, +# close open rows silent 5+ min and reopen closed rows still +# reporting (freshness-guarded so replayed timestamps cannot +# resurrect dead rows). Adds idx_radacct_stop_update +# (acctstoptime, acctupdatetime) so both statements stay cheap. # --------------------------------------------------------------------------------- set -euo pipefail @@ -472,6 +479,7 @@ declare -a IDX_SPECS=( "radacct|idx_radacct_user_time|username(50),acctstarttime" "radacct|idx_radacct_nas_ip|nasipaddress" "radacct|idx_radacct_framed|framedipaddress" + "radacct|idx_radacct_stop_update|acctstoptime,acctupdatetime" "radpostauth|idx_authdate|authdate" "radcheck|idx_username|username" "radreply|idx_username_attr|username,attribute" @@ -989,6 +997,88 @@ fi log "" +# ---------------------------------------------------------------------- +# Step 6e: radacct reconcile cron (open/closed state <- 1-min interims). +# With interim-update=1m on every NAS, freshness of acctupdatetime IS +# session liveness. Every minute, converge radacct in both directions: +# close - open rows silent for 5+ minutes (Stop was lost); +# reopen - closed rows still reporting (a sweep or bulk close caught +# a live session; interims update closed rows but nothing +# else ever reopens them). +# The pair is self-correcting: a server outage that lets rows go silent +# closes them, and the first interim after recovery reopens the ones +# still alive. The reopen's freshness guard keeps replayed/backdated +# timestamps from resurrecting dead rows. Idempotent. +# ---------------------------------------------------------------------- +log "========================" +log " Step 6e: radacct reconcile cron (1-min interim liveness)" +log "------------------------" + +RECON_BIN="/usr/local/sbin/radacct-reconcile.sh" +RECON_CRON="/etc/cron.d/radacct-reconcile" +RECON_CHANGED=0 + +RECON_TMP="${RECON_BIN}.new" +cat > "${RECON_TMP}" << 'RECON' +#!/bin/bash +# Installed by universal.sh (Step 6e). Converges radacct's open/closed +# state with reality, scaled to the 1-minute interim cadence: a session +# silent for 5+ minutes is dead; a "closed" row still receiving fresh +# interims is alive. Both statements are idempotent and cheap (covered +# by idx_radacct_stop_update). +set -u + +exec 9>"/var/lock/radacct-reconcile.lock" +flock -n 9 || exit 0 +command -v mysql >/dev/null 2>&1 || exit 0 + +mysql radius -e " +UPDATE radacct SET acctstoptime = acctupdatetime, acctterminatecause = 'Stale-Session' + WHERE acctstoptime IS NULL + AND acctupdatetime < NOW() - INTERVAL 5 MINUTE; +UPDATE radacct SET acctstoptime = NULL, acctterminatecause = '' + WHERE acctstoptime IS NOT NULL + AND acctupdatetime > NOW() - INTERVAL 5 MINUTE + AND acctupdatetime > acctstoptime + INTERVAL 1 MINUTE; +" 2>/dev/null +RECON + +RECON_CRON_TMP="${RECON_CRON}.new" +cat > "${RECON_CRON_TMP}" << 'EOF' +# Installed by universal.sh (Step 6e): converge radacct open/closed state +# with the 1-minute interim heartbeat. +* * * * * root /usr/local/sbin/radacct-reconcile.sh +EOF + +recon_install_if_changed() { + # $1 = live file, $2 = desired temp file, $3 = mode. Returns 0 if a + # change was made (or would be, under --dry-run), 1 if up to date. + local live="$1" tmp="$2" mode="$3" + if [ -f "${live}" ] && cmp -s "${live}" "${tmp}"; then + rm -f "${tmp}" || true + chmod "${mode}" "${live}" 2>/dev/null || true + return 1 + fi + if [ "$DRY_RUN" -eq 0 ]; then + mv -f "${tmp}" "${live}" + chmod "${mode}" "${live}" + log "[OK] installed ${live}" + else + rm -f "${tmp}" || true + log "[DRY] would install ${live}" + fi + return 0 +} + +if recon_install_if_changed "${RECON_BIN}" "${RECON_TMP}" 0755; then RECON_CHANGED=1; fi +if recon_install_if_changed "${RECON_CRON}" "${RECON_CRON_TMP}" 0644; then RECON_CHANGED=1; fi + +if [ "${RECON_CHANGED}" -eq 0 ]; then + log "[OK] radacct reconcile cron already installed; no change" +fi + +log "" + # --------------------------------------------------------------------- # Step 7: PHP-FPM detection, tuning, slowlog setup, and IN-PLACE edits # --------------------------------------------------------------------- From 213d124fa729632f8b993f210d5c9f501b1cd3b4 Mon Sep 17 00:00:00 2001 From: Barnabie Opiyo Date: Thu, 23 Jul 2026 23:00:55 +0300 Subject: [PATCH 43/45] Never write backups into FreeRADIUS $INCLUDEd directories (v3.5.2) radiusd.conf does "$INCLUDE sites-enabled/" (and mods-enabled/), so the .bak.TIMESTAMP files Step 6b wrote next to sites-enabled/default and sites-enabled/buffered-sql were parsed as config: "Duplicate virtual server 'default'" made freeradius fail to start on the next restart or nightly reboot, while the step's own -XC validation kept failing and rolling back the conversion - leaving servers unconverted AND unable to restart freeradius. - All backups of files living in sites-enabled/ or mods-enabled/ now go to CONF_BACKUP_DIR (pruned by the existing Step 3 retention), tracked as live:backup pairs and restored from there on failed validation - Step 6b sweeps stray *.bak.* files out of both directories first, which by itself repairs servers already stuck in the broken state, and triggers validation + restart so they recover in the same run Verified with 15 functional checks in WSL: damaged-tree recovery (stray swept, direct sql conversion, re-link, buffered wiring disabled with relocated backups), idempotent rerun, rollback that restores everything except the sweep, and dry-run no-op. Co-Authored-By: Claude Fable 5 --- universal.sh | 46 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/universal.sh b/universal.sh index b969343..10bb438 100644 --- a/universal.sh +++ b/universal.sh @@ -71,6 +71,13 @@ # reporting (freshness-guarded so replayed timestamps cannot # resurrect dead rows). Adds idx_radacct_stop_update # (acctstoptime, acctupdatetime) so both statements stay cheap. +# +# v3.5.2 (2026-07-23): FreeRADIUS $INCLUDEs sites-enabled/ and +# mods-enabled/ wholesale, so the .bak files earlier versions wrote +# next to live files there were parsed as duplicate servers and +# freeradius could not start ("Duplicate virtual server"). Backups of +# files in those directories now go to CONF_BACKUP_DIR, and Step 6b +# sweeps strays left by v3.1-v3.5 - repairing servers stuck failing. # --------------------------------------------------------------------------------- set -euo pipefail @@ -766,6 +773,23 @@ else BUFSQL_TOUCHED=() # files backed up this run, restored on failed validation BUFSQL_NEW_LINKS=() # symlinks created this run, removed on failed validation BUFSQL_REMOVED_LINKS=() # "link:target" removed this run, re-created on failed validation + BUFSQL_MOVED=() # "live:backup" pairs whose backup lives in CONF_BACKUP_DIR + + # FreeRADIUS $INCLUDEs sites-enabled/ and mods-enabled/ wholesale, so ANY + # extra file there - including a backup - is parsed, and a duplicated + # virtual server kills startup (bug in v3.1-v3.5: backups were written + # next to the live file). Sweep strays into the backup dir; this alone + # repairs servers that could no longer restart freeradius. + for stray in "${RAD_ROOT}"/sites-enabled/*.bak.* "${RAD_ROOT}"/mods-enabled/*.bak.*; do + { [ -e "$stray" ] || [ -L "$stray" ]; } || continue + if [ "$DRY_RUN" -eq 0 ]; then + mv -f "$stray" "${CONF_BACKUP_DIR}/" || true + log "[OK] moved stray backup ${stray} out of \$INCLUDEd dir -> ${CONF_BACKUP_DIR}/" + else + log "[DRY] would move stray backup ${stray} -> ${CONF_BACKUP_DIR}/" + fi + BUFSQL_CHANGED=1 + done # Desired default site, matching the installers exactly: -sql refs # enabled and the accounting section writing straight to sql. @@ -840,9 +864,11 @@ else if [ ! -L "${RAD_DEFAULT_LINK}" ]; then if [ "$DRY_RUN" -eq 0 ]; then if [ -f "${RAD_DEFAULT_LINK}" ]; then - cp -a "${RAD_DEFAULT_LINK}" "${RAD_DEFAULT_LINK}.bak.${TIMESTAMP}" - prune_baks "${RAD_DEFAULT_LINK}" - BUFSQL_TOUCHED+=("${RAD_DEFAULT_LINK}") + # Backup must NOT live in sites-enabled/ - FreeRADIUS $INCLUDEs + # that directory and would parse it as a duplicate server. + RAD_LINK_BAK="${CONF_BACKUP_DIR}/sites-enabled-default_${TIMESTAMP}" + cp -a "${RAD_DEFAULT_LINK}" "${RAD_LINK_BAK}" + BUFSQL_MOVED+=("${RAD_DEFAULT_LINK}:${RAD_LINK_BAK}") rm -f "${RAD_DEFAULT_LINK}" else BUFSQL_NEW_LINKS+=("${RAD_DEFAULT_LINK}") @@ -867,10 +893,11 @@ else BUFSQL_REMOVED_LINKS+=("${BUFSQL_DST}:$(readlink "${BUFSQL_DST}")") rm -f "${BUFSQL_DST}" else - # Older installs sometimes left a regular file here. - cp -a "${BUFSQL_DST}" "${BUFSQL_DST}.bak.${TIMESTAMP}" - prune_baks "${BUFSQL_DST}" - BUFSQL_TOUCHED+=("${BUFSQL_DST}") + # Older installs sometimes left a regular file here. Its backup + # must NOT stay in this $INCLUDEd directory (see sweep above). + BUFSQL_FILE_BAK="${CONF_BACKUP_DIR}/$(basename "${BUFSQL_DST}")_${TIMESTAMP}" + cp -a "${BUFSQL_DST}" "${BUFSQL_FILE_BAK}" + BUFSQL_MOVED+=("${BUFSQL_DST}:${BUFSQL_FILE_BAK}") rm -f "${BUFSQL_DST}" fi log "[OK] disabled ${BUFSQL_DST}" @@ -895,6 +922,11 @@ else if [ -f "${f}.bak.${TIMESTAMP}" ]; then rm -f "${f}"; cp -a "${f}.bak.${TIMESTAMP}" "${f}"; fi done for l in "${BUFSQL_NEW_LINKS[@]}"; do rm -f "${l}" || true; done + for m in "${BUFSQL_MOVED[@]}"; do + # rm first so a path that became a symlink this run is replaced, + # not written through. + rm -f "${m%%:*}"; cp -a "${m#*:}" "${m%%:*}" || true + done for l in "${BUFSQL_REMOVED_LINKS[@]}"; do ln -sf "${l#*:}" "${l%%:*}" || true; done else log "[OK] direct accounting wiring applied and validated" From cc1eb2299a6d818e0d881a71f32055d48b3c61d4 Mon Sep 17 00:00:00 2001 From: Barnabie Opiyo Date: Thu, 23 Jul 2026 23:10:07 +0300 Subject: [PATCH 44/45] Recover a down FreeRADIUS at end of run, not only on config change The end-of-run restart only fired when RADIUS_CHANGED=1, so a server whose freeradius failed at boot (e.g. stray sites-enabled backups pre-v3.5.2) stayed down if the current run had nothing to converge. universal.sh fires @reboot, making it the natural recovery point: if neither freeradius nor radiusd is active and the config passes -XC, attempt a start; if the config fails validation, log an explicit manual-attention error instead. Skipped under --dry-run. Co-Authored-By: Claude Fable 5 --- universal.sh | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/universal.sh b/universal.sh index 10bb438..cb665bb 100644 --- a/universal.sh +++ b/universal.sh @@ -1431,7 +1431,24 @@ log "[OK] MariaDB: dynamic settings already applied live (Step 5); no restart" log "[OK] Valkey: settings already applied live (Step 5b); no restart" log "[OK] PHP-FPM: reload handled in Step 7 (only when pool config changed)" -if [ "${RADIUS_CHANGED:-0}" -eq 1 ]; then +# Also recover a DOWN freeradius even when nothing changed this run: +# universal.sh fires @reboot, so this brings the service back after a +# failed boot-time start (e.g. stray sites-enabled backups pre-v3.5.2) +# once the config validates again. +RADIUS_DOWN=0 +if [ "$DRY_RUN" -eq 0 ] \ + && ! systemctl is-active --quiet freeradius 2>/dev/null \ + && ! systemctl is-active --quiet radiusd 2>/dev/null; then + RAD_BIN="$(command -v freeradius || command -v radiusd || true)" + if [ -n "${RAD_BIN}" ] && "${RAD_BIN}" -XC >/dev/null 2>&1; then + RADIUS_DOWN=1 + log "[WARN] FreeRADIUS is not running but the config validates; attempting recovery start" + elif [ -n "${RAD_BIN}" ]; then + log "[ERROR] FreeRADIUS is not running and the config FAILS validation (${RAD_BIN} -XC); manual attention required" + fi +fi + +if [ "${RADIUS_CHANGED:-0}" -eq 1 ] || [ "${RADIUS_DOWN}" -eq 1 ]; then RAD_RESTARTED=0 for candidate in freeradius radiusd; do if restart_and_check "$candidate"; then From 486dda803aa66d67cd8e06db135b18b6eba95dd5 Mon Sep 17 00:00:00 2001 From: Barnabie Opiyo Date: Thu, 23 Jul 2026 23:17:11 +0300 Subject: [PATCH 45/45] Installers: sweep stray backups and retired buffered wiring before -XC Reinstalling over a server damaged by universal.sh v3.1-v3.5 (stray .bak files in sites-enabled/, parsed by FreeRADIUS as duplicate virtual servers) aborted at the installers' -XC validation. Before validating, move stray *.bak.* files out of sites-enabled/ and mods-enabled/ into /var/backups/universal/conf, and drop the retired buffered-sql site / detail module links so they do not survive a reinstall. Mirrors universal.sh v3.5.2. Co-Authored-By: Claude Fable 5 --- ubuntu_simpleisp.sh | 19 +++++++++++++++++++ ubuntu_simplespot.sh | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/ubuntu_simpleisp.sh b/ubuntu_simpleisp.sh index 0744fe9..40b3fa0 100644 --- a/ubuntu_simpleisp.sh +++ b/ubuntu_simpleisp.sh @@ -1040,6 +1040,25 @@ else fi COMPLETED_STEPS+=("FreeRADIUS default site configured") +# Clean the $INCLUDEd dirs before validating: FreeRADIUS parses EVERY file +# in sites-enabled/ and mods-enabled/, so stray .bak files (written there +# by universal.sh v3.1-v3.5) fail startup with "Duplicate virtual server", +# and the retired buffered-sql/detail links should not survive a reinstall. +log_step "Cleaning FreeRADIUS enabled dirs (retired buffered wiring, stray backups)" +mkdir -p /var/backups/universal/conf +for stale in "${FREERADIUS_CONF_DIR}/sites-enabled/buffered-sql" "${FREERADIUS_CONF_DIR}/mods-enabled/detail"; do + if [ -L "$stale" ]; then + rm -f "$stale" + elif [ -f "$stale" ]; then + mv -f "$stale" /var/backups/universal/conf/ || rm -f "$stale" + fi +done +for stray in "${FREERADIUS_CONF_DIR}"/sites-enabled/*.bak.* "${FREERADIUS_CONF_DIR}"/mods-enabled/*.bak.*; do + { [ -e "$stray" ] || [ -L "$stray" ]; } || continue + mv -f "$stray" /var/backups/universal/conf/ || rm -f "$stray" +done +COMPLETED_STEPS+=("FreeRADIUS enabled dirs cleaned") + # Validate the FreeRADIUS configuration now so wiring mistakes fail loudly # here instead of at the service restart later. log_step "Validating FreeRADIUS configuration" diff --git a/ubuntu_simplespot.sh b/ubuntu_simplespot.sh index 9ccec8d..9cd2908 100644 --- a/ubuntu_simplespot.sh +++ b/ubuntu_simplespot.sh @@ -1037,6 +1037,25 @@ else fi COMPLETED_STEPS+=("FreeRADIUS default site configured") +# Clean the $INCLUDEd dirs before validating: FreeRADIUS parses EVERY file +# in sites-enabled/ and mods-enabled/, so stray .bak files (written there +# by universal.sh v3.1-v3.5) fail startup with "Duplicate virtual server", +# and the retired buffered-sql/detail links should not survive a reinstall. +log_step "Cleaning FreeRADIUS enabled dirs (retired buffered wiring, stray backups)" +mkdir -p /var/backups/universal/conf +for stale in "${FREERADIUS_CONF_DIR}/sites-enabled/buffered-sql" "${FREERADIUS_CONF_DIR}/mods-enabled/detail"; do + if [ -L "$stale" ]; then + rm -f "$stale" + elif [ -f "$stale" ]; then + mv -f "$stale" /var/backups/universal/conf/ || rm -f "$stale" + fi +done +for stray in "${FREERADIUS_CONF_DIR}"/sites-enabled/*.bak.* "${FREERADIUS_CONF_DIR}"/mods-enabled/*.bak.*; do + { [ -e "$stray" ] || [ -L "$stray" ]; } || continue + mv -f "$stray" /var/backups/universal/conf/ || rm -f "$stray" +done +COMPLETED_STEPS+=("FreeRADIUS enabled dirs cleaned") + # Validate the FreeRADIUS configuration now so wiring mistakes fail loudly # here instead of at the service restart later. log_step "Validating FreeRADIUS configuration"