#!/bin/bash # NAS Setup Script - Version 2.1.1 # # This script automates the setup of a NAS system with various services. # It is designed to run on multiple Linux distributions, including: # - Ubuntu 24.04+ # - Debian 12+ # - Fedora 41+ # - Arch Linux # - openSUSE Leap 15.6+ # # Disclaimer: # This script is provided "as is", without warranty of any kind, express or implied, # including but not limited to the warranties of merchantability, fitness for a particular purpose, # and noninfringement. In no event shall the authors or copyright holders be liable for any claim, # damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, # out of, or in connection with the software or the use or other dealings in the software. # # Usage: # Run this script with root privileges on a fresh installation of a supported Linux distribution. # Ensure you have an active internet connection before starting the setup. # # Author: Sebastian Palencsár # License: MIT License # (c) 2025 Sebastian Palencsár set -euo pipefail # Strict error handling # Script directory and imports SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # Import configuration and functions source "${SCRIPT_DIR}/config/defaults.sh" source "${SCRIPT_DIR}/lib/logging.sh" source "${SCRIPT_DIR}/lib/common.sh" source "${SCRIPT_DIR}/lib/detection.sh" source "${SCRIPT_DIR}/lib/network.sh" source "${SCRIPT_DIR}/lib/docker.sh" source "${SCRIPT_DIR}/lib/security.sh" source "${SCRIPT_DIR}/lib/internet.sh" source "${SCRIPT_DIR}/lib/nfs.sh" source "${SCRIPT_DIR}/lib/netdata.sh" source "${SCRIPT_DIR}/lib/firewall.sh" source "${SCRIPT_DIR}/lib/unattended-upgrades.sh" source "${SCRIPT_DIR}/lib/vaultwarden.sh" source "${SCRIPT_DIR}/lib/jellyfin.sh" source "${SCRIPT_DIR}/lib/portainer.sh" source "${SCRIPT_DIR}/lib/webmin.sh" source "${SCRIPT_DIR}/lib/performance.sh" # Initialize logging mkdir -p "$(dirname "${LOG_FILE}")" mkdir -p "${TEMP_DIR}" # Setup logging with rotation exec > >(tee -a "${LOG_FILE}") 2>&1 # Enhanced error handling with rollback handle_error() { # Dual-mode: if called with a non-numeric first argument, treat as wrapper # e.g. handle_error sudo cp ... -> execute the command and handle failures if [[ $# -ge 1 && ! "$1" =~ ^[0-9]+$ ]]; then "$@" local _rc=$? if [[ $_rc -ne 0 ]]; then local _line_number=${BASH_LINENO[0]:-0} local _command="$*" log_error "Script failed at line ${_line_number}: ${_command} (exit code: ${_rc})" if ask_yes_no "An error occurred. Would you like to rollback changes?" "y"; then execute_rollback fi cleanup exit $_rc fi return 0 fi # Trap handler mode: handle_error "" local exit_code=$? local line_number=$1 local command="$2" log_error "Script failed at line ${line_number}: ${command} (exit code: ${exit_code})" if ask_yes_no "An error occurred. Would you like to rollback changes?" "y"; then execute_rollback fi cleanup exit $exit_code } # Set error trap trap 'handle_error ${LINENO} "$BASH_COMMAND"' ERR # Signal handlers cleanup_on_exit() { log_info "Script interrupted. Performing cleanup..." cleanup exit 130 } trap cleanup_on_exit SIGINT SIGTERM # Detect Linux distribution with comprehensive fallback methods detect_distro() { local detected_distro="" local detected_version="" local detected_codename="" local detection_method="" log_debug "Starting distribution detection..." # Method 1: /etc/os-release (primary method for modern systems) if [[ -f /etc/os-release ]]; then source /etc/os-release 2>/dev/null || true detected_distro=${ID,,} # Convert to lowercase detected_version=$VERSION_ID detected_codename=${VERSION_CODENAME:-${UBUNTU_CODENAME:-""}} detection_method="/etc/os-release" log_debug "Detected via /etc/os-release: $PRETTY_NAME" fi # Method 2: /etc/redhat-release (fallback for RHEL/CentOS/Fedora) if [[ -z "$detected_distro" ]] && [[ -f /etc/redhat-release ]]; then local redhat_info=$(cat /etc/redhat-release) if [[ $redhat_info =~ ^(CentOS|Red Hat Enterprise|Fedora) ]]; then detected_distro="fedora" detected_version=$(echo "$redhat_info" | grep -oP '\d+\.\d+' | head -1) detection_method="/etc/redhat-release" log_debug "Detected via /etc/redhat-release: $redhat_info" fi fi # Method 3: /etc/debian_version (fallback for Debian/Ubuntu) if [[ -z "$detected_distro" ]] && [[ -f /etc/debian_version ]]; then local debian_version=$(cat /etc/debian_version) if [[ -f /etc/lsb-release ]]; then source /etc/lsb-release 2>/dev/null || true detected_distro=${DISTRIB_ID,,} detected_version=$DISTRIB_RELEASE detected_codename=${DISTRIB_CODENAME:-""} detection_method="/etc/lsb-release" else # Pure Debian system detected_distro="debian" detected_version=$debian_version detection_method="/etc/debian_version" fi log_debug "Detected via Debian method: $detected_distro $detected_version" fi # Method 4: lsb_release command (fallback) if [[ -z "$detected_distro" ]] && command -v lsb_release >/dev/null 2>&1; then detected_distro=$(lsb_release -si 2>/dev/null | tr '[:upper:]' '[:lower:]') detected_version=$(lsb_release -sr 2>/dev/null) detected_codename=$(lsb_release -sc 2>/dev/null) detection_method="lsb_release command" log_debug "Detected via lsb_release command: $detected_distro $detected_version" fi # Method 5: uname and manual detection (last resort) if [[ -z "$detected_distro" ]]; then if [[ -f /etc/arch-release ]]; then detected_distro="arch" detected_version="rolling" detection_method="/etc/arch-release" elif [[ -f /etc/gentoo-release ]]; then detected_distro="gentoo" detected_version=$(cat /etc/gentoo-release | grep -oP '\d+\.\d+' | head -1) detection_method="/etc/gentoo-release" elif uname -a | grep -qi "opensuse"; then detected_distro="opensuse" detected_version="unknown" detection_method="uname opensuse" fi log_debug "Detected via fallback method: $detected_distro" fi # Validate detection if [[ -z "$detected_distro" ]]; then log_error "Failed to detect Linux distribution using all available methods" log_error "Please check your system and ensure it's a supported Linux distribution" log_error "Supported: ${SUPPORTED_DISTROS[*]}" exit 1 fi # Normalize distribution names case $detected_distro in ubuntu|debian|fedora|arch|opensuse) DISTRO=$detected_distro ;; "red hat enterprise linux server"|"rhel") DISTRO="fedora" # Treat RHEL as Fedora for package management ;; "centos linux"|"centos") DISTRO="fedora" # CentOS uses same package manager as Fedora ;; *) # Check if it's a known variant if [[ " ${SUPPORTED_DISTROS[*]} " =~ " ${detected_distro} " ]]; then DISTRO=$detected_distro else log_error "Detected distribution '$detected_distro' is not in supported list" log_error "Supported distributions: ${SUPPORTED_DISTROS[*]}" log_error "Detection method: $detection_method" exit 1 fi ;; esac # Parse and normalize version DISTRO_VERSION=$(normalize_version "$detected_version") DISTRO_CODENAME=$detected_codename log_info "Distribution detected: $DISTRO $DISTRO_VERSION ($detection_method)" if [[ -n "$DISTRO_CODENAME" ]]; then log_debug "Codename: $DISTRO_CODENAME" fi # Validate supported distribution if [[ ! " ${SUPPORTED_DISTROS[*]} " =~ " ${DISTRO} " ]]; then log_error "Unsupported Linux distribution: $DISTRO" log_info "Supported distributions: ${SUPPORTED_DISTROS[*]}" log_info "Detection method: $detection_method" exit 1 fi # Check minimum versions with improved parsing validate_minimum_version "$DISTRO" "$DISTRO_VERSION" # Check for container environments detect_container_environment # Cache the results for performance export DISTRO DETECTED_DISTRO=$DISTRO export DISTRO_VERSION DETECTED_VERSION=$DISTRO_VERSION export DISTRO_CODENAME DETECTED_CODENAME=$DISTRO_CODENAME } # System requirements check check_system_requirements() { log_info "Checking system requirements..." local requirements_met=true # Check disk space if ! check_disk_space $MIN_DISK_SPACE_GB; then requirements_met=false fi # Check RAM check_ram $MIN_RAM_MB # Check if running as root check_root # Check internet connectivity if ! check_internet_enhanced; then requirements_met=false fi # Check architecture local arch=$(uname -m) case $arch in x86_64|amd64) log_info "Architecture check passed: $arch" ;; aarch64|arm64) log_warning "ARM64 architecture detected. Some packages may not be available." ;; *) log_error "Unsupported architecture: $arch" requirements_met=false ;; esac if [[ "$requirements_met" == false ]]; then log_error "System requirements not met. Exiting." exit 1 fi log_success "All system requirements met" } # Configuration management load_or_create_config() { log_info "Loading configuration..." if load_config; then log_info "Configuration loaded from ${CONFIG_FILE}" if ! validate_config; then log_warning "Configuration validation failed - creating new configuration" create_interactive_config load_config # Reload the new configuration fi else log_info "Creating new configuration..." create_interactive_config load_config # Load the new configuration fi } create_interactive_config() { log_info "Interactive configuration setup" echo # SSH configuration local ssh_port=$(ask_input "SSH port" "$DEFAULT_SSH_PORT" "validate_port") save_config "SSH_PORT" "$ssh_port" # User configuration local current_user if [[ -n "${SUDO_USER:-}" ]]; then current_user="$SUDO_USER" log_info "Using current sudo user: $current_user" else current_user="$USER" log_info "Using current user: $current_user" fi # Pre-fill with current user, but allow override local username=$(ask_input "Admin username" "$current_user" "validate_username") save_config "ADMIN_USER" "$username" # Ensure the configured admin user exists (offer interactive creation) ensure_user_exists_interactive "$username" || log_warning "Admin user '$username' not created; some features may require it." # Network configuration if ask_yes_no "Configure static IP?" "n"; then save_config "CONFIGURE_STATIC_IP" "true" else save_config "CONFIGURE_STATIC_IP" "false" fi # Service selection local install_docker=$(ask_yes_no "Install Docker?" "y" && echo "true" || echo "false") save_config "INSTALL_DOCKER" "$install_docker" save_config "INSTALL_NFS" "$(ask_yes_no "Install NFS?" "n" && echo "true" || echo "false")" save_config "INSTALL_NETDATA" "$(ask_yes_no "Install Netdata monitoring?" "y" && echo "true" || echo "false")" # Docker-dependent services if [[ "$install_docker" == "true" ]]; then save_config "INSTALL_VAULTWARDEN" "$(ask_yes_no "Install Vaultwarden password manager?" "n" && echo "true" || echo "false")" save_config "INSTALL_JELLYFIN" "$(ask_yes_no "Install Jellyfin media server?" "n" && echo "true" || echo "false")" save_config "INSTALL_PORTAINER" "$(ask_yes_no "Install Portainer Docker management?" "n" && echo "true" || echo "false")" else log_warning "Docker not selected - skipping Docker-dependent services (Vaultwarden, Jellyfin, Portainer)" save_config "INSTALL_VAULTWARDEN" "false" save_config "INSTALL_JELLYFIN" "false" save_config "INSTALL_PORTAINER" "false" fi # Automatic updates (optional) save_config "ENABLE_AUTO_UPDATES" "$(ask_yes_no "Enable automatic security updates (unattended-upgrades)?" "n" && echo "true" || echo "false")" save_config "INSTALL_WEBMIN" "$(ask_yes_no "Install Webmin web interface?" "n" && echo "true" || echo "false")" log_success "Configuration created and saved to ${CONFIG_FILE}" } # System update with progress update_system() { log_info "Updating system packages..." show_progress 1 10 "System Update" case $DISTRO in ubuntu|debian) sudo apt-get update -qq show_progress 5 10 "System Update" sudo apt-get upgrade -y -qq ;; fedora) sudo dnf update -y -q ;; arch) sudo pacman -Syu --noconfirm --quiet ;; opensuse) sudo zypper refresh -q sudo zypper update -y -q ;; esac show_progress 10 10 "System Update" add_rollback_action "# System packages updated - no rollback needed" } # Main installation orchestrator run_installation() { local total_steps=13 local current_step=0 log_info "Starting NAS installation process..." # Core system setup current_step=$((current_step + 1)); log_debug "run_installation: about to install dependencies (step $current_step/$total_steps)"; show_progress $current_step $total_steps "Installing dependencies" install_dependencies current_step=$((current_step + 1)); log_debug "run_installation: about to configure network (step $current_step/$total_steps)"; show_progress $current_step $total_steps "Configuring network" if [[ "${CONFIGURE_STATIC_IP:-false}" == "true" ]]; then configure_network fi current_step=$((current_step + 1)); log_debug "run_installation: about to configure SSH (step $current_step/$total_steps)"; show_progress $current_step $total_steps "Configuring SSH" configure_ssh current_step=$((current_step + 1)); log_debug "run_installation: about to setup Samba (step $current_step/$total_steps)"; show_progress $current_step $total_steps "Setting up Samba" setup_samba current_step=$((current_step + 1)); log_debug "run_installation: about to configure firewall (step $current_step/$total_steps)"; show_progress $current_step $total_steps "Configuring firewall" configure_firewall # Security setup current_step=$((current_step + 1)); log_debug "run_installation: about to implement security measures (step $current_step/$total_steps)"; show_progress $current_step $total_steps "Implementing security measures" secure_shared_memory install_fail2ban # Configure automatic updates only if user opted in if [[ "${ENABLE_AUTO_UPDATES:-false}" == "true" ]]; then configure_automatic_updates else log_info "Automatic security updates are disabled by configuration" fi # Optional services if [[ "${INSTALL_DOCKER:-false}" == "true" ]]; then current_step=$((current_step + 1)); log_debug "run_installation: about to install Docker (step $current_step/$total_steps)"; show_progress $current_step $total_steps "Installing Docker" install_docker else current_step=$((current_step + 1)) fi if [[ "${INSTALL_NFS:-false}" == "true" ]]; then current_step=$((current_step + 1)); log_debug "run_installation: about to install NFS (step $current_step/$total_steps)"; show_progress $current_step $total_steps "Installing NFS" install_nfs else current_step=$((current_step + 1)) fi if [[ "${INSTALL_NETDATA:-false}" == "true" ]]; then current_step=$((current_step + 1)); log_debug "run_installation: about to install Netdata (step $current_step/$total_steps)"; show_progress $current_step $total_steps "Installing Netdata" install_netdata else current_step=$((current_step + 1)) fi if [[ "${INSTALL_VAULTWARDEN:-false}" == "true" ]]; then current_step=$((current_step + 1)); log_debug "run_installation: about to install Vaultwarden (step $current_step/$total_steps)"; show_progress $current_step $total_steps "Installing Vaultwarden" install_vaultwarden else current_step=$((current_step + 1)) fi if [[ "${INSTALL_JELLYFIN:-false}" == "true" ]]; then current_step=$((current_step + 1)); log_debug "run_installation: about to install Jellyfin (step $current_step/$total_steps)"; show_progress $current_step $total_steps "Installing Jellyfin" install_jellyfin else current_step=$((current_step + 1)) fi if [[ "${INSTALL_PORTAINER:-false}" == "true" ]]; then current_step=$((current_step + 1)); log_debug "run_installation: about to install Portainer (step $current_step/$total_steps)"; show_progress $current_step $total_steps "Installing Portainer" install_portainer else current_step=$((current_step + 1)) fi if [[ "${INSTALL_WEBMIN:-false}" == "true" ]]; then current_step=$((current_step + 1)); log_debug "run_installation: about to install Webmin (step $current_step/$total_steps)"; show_progress $current_step $total_steps "Installing Webmin" install_webmin configure_webmin else current_step=$((current_step + 1)) fi } # Installation summary show_installation_summary() { local ip_address ip_address=$(hostname -I | awk '{print $1}') local user_home if [[ -n "${SUDO_USER:-}" ]]; then user_home=$(eval echo "~${SUDO_USER}") else user_home="$HOME" fi local summary_file="${user_home}/nas_services.txt" echo log_success "=== NAS Setup Completed Successfully ===" echo log_info "Installation Summary:" echo " ✓ System updated and secured" echo " ✓ User '${ADMIN_USER:-$NEW_USER}' created with sudo access" echo " ✓ SSH configured on port ${SSH_PORT:-$DEFAULT_SSH_PORT}" echo " ✓ Samba file sharing configured" echo " ✓ Firewall configured and enabled" echo " ✓ Fail2ban installed and configured" if [[ "${ENABLE_AUTO_UPDATES:-false}" == "true" ]]; then echo " ✓ Automatic updates enabled" else echo " - Automatic updates not enabled (optional)" fi # Service summary if [[ "${INSTALL_DOCKER:-false}" == "true" ]]; then echo " ✓ Docker installed and configured" fi if [[ "${INSTALL_NFS:-false}" == "true" ]]; then echo " ✓ NFS server installed" fi if [[ "${INSTALL_NETDATA:-false}" == "true" ]]; then echo " ✓ Netdata monitoring: http://${ip_address}:${NETDATA_PORT}" fi if [[ "${INSTALL_JELLYFIN:-false}" == "true" ]]; then echo " ✓ Jellyfin media server: http://${ip_address}:8096" fi if [[ "${INSTALL_PORTAINER:-false}" == "true" ]]; then echo " ✓ Portainer Docker management: http://${ip_address}:9000" fi if [[ "${INSTALL_WEBMIN:-false}" == "true" ]]; then echo " ✓ Webmin web interface: https://${ip_address}:10000" fi # Create services summary file log_info "Creating services summary file: ${summary_file}" cat > "${summary_file}" << EOF ============================================================ NAS Services - Installation Summary ============================================================ Installation Date: $(date) System IP Address: ${ip_address} Admin User: ${ADMIN_USER:-$NEW_USER} SSH Port: ${SSH_PORT:-$DEFAULT_SSH_PORT} ------------------------------------------------------------ Installed Services and Access Information: ------------------------------------------------------------ SSH Access: - Host: ${ip_address} - Port: ${SSH_PORT:-$DEFAULT_SSH_PORT} - User: ${ADMIN_USER:-$NEW_USER} - Command: ssh -p ${SSH_PORT:-$DEFAULT_SSH_PORT} ${ADMIN_USER:-$NEW_USER}@${ip_address} EOF # Add service information to file if [[ "${INSTALL_NETDATA:-false}" == "true" ]]; then cat >> "${summary_file}" << EOF Netdata Monitoring: - URL: http://${ip_address}:${NETDATA_PORT} - Description: Real-time system monitoring and metrics EOF fi if [[ "${INSTALL_JELLYFIN:-false}" == "true" ]]; then cat >> "${summary_file}" << EOF Jellyfin Media Server: - Web Interface: http://${ip_address}:8096 - HTTPS Interface: http://${ip_address}:8920 - DLNA Port: ${ip_address}:1900 (UDP) - Description: Media streaming and management server EOF fi if [[ "${INSTALL_PORTAINER:-false}" == "true" ]]; then cat >> "${summary_file}" << EOF Portainer Docker Management: - HTTP Interface: http://${ip_address}:9000 - HTTPS Interface: https://${ip_address}:9443 - Description: Web-based Docker container management - Note: Use HTTPS (9443) for secure access EOF fi if [[ "${INSTALL_VAULTWARDEN:-false}" == "true" ]]; then cat >> "${summary_file}" << EOF Vaultwarden Password Manager: - URL: http://${ip_address}:8080 - Description: Bitwarden-compatible password manager EOF fi if [[ "${INSTALL_WEBMIN:-false}" == "true" ]]; then cat >> "${summary_file}" << EOF Webmin System Administration: - URL: https://${ip_address}:10000 - Description: Web-based system administration interface EOF fi if [[ "${INSTALL_NFS:-false}" == "true" ]]; then cat >> "${summary_file}" << EOF NFS Server: - Port: ${ip_address}:2049 - Description: Network File System for Unix/Linux clients - Mount example: mount -t nfs ${ip_address}:/srv/nfs /mnt/nfs EOF fi # Samba information cat >> "${summary_file}" << EOF Samba File Sharing: - Ports: ${ip_address}:139 (TCP), ${ip_address}:445 (TCP), ${ip_address}:137-138 (UDP) - Description: Windows file sharing (SMB/CIFS) - Access: \\\\${ip_address} or smb://${ip_address} EOF # Docker information if installed if [[ "${INSTALL_DOCKER:-false}" == "true" ]]; then cat >> "${summary_file}" << EOF Docker: - Status: Installed and running - Socket: /var/run/docker.sock - Description: Container runtime for additional services EOF fi cat >> "${summary_file}" << EOF ------------------------------------------------------------ System Information: ------------------------------------------------------------ - Configuration file: ${CONFIG_FILE} - Installation log: ${LOG_FILE} - System distribution: ${DISTRO_NAME} ${DISTRO_VERSION} - Kernel: $(uname -r) ------------------------------------------------------------ Security Notes: ------------------------------------------------------------ - Firewall is active and configured - Fail2ban is monitoring for suspicious activity - SSH uses Ed25519 keys for enhanced security - Root login via SSH is disabled EOF if [[ "${ENABLE_AUTO_UPDATES:-false}" == "true" ]]; then echo "- Automatic security updates are enabled" >> "${summary_file}" else echo "- Automatic updates are disabled (manual updates recommended)" >> "${summary_file}" fi cat >> "${summary_file}" << EOF ------------------------------------------------------------ Next Steps: ------------------------------------------------------------ 1. Reboot the system to ensure all changes take effect 2. Review the services above and access them using the provided URLs 3. Configure user accounts and permissions for file sharing 4. Set up backups for your important data 5. Regularly check system logs and monitoring ============================================================ EOF log_success "Services summary saved to: ${summary_file}" log_info "You can view this file with: cat ${summary_file}" echo log_info "Next steps:" echo " 1. Reboot the system to ensure all changes take effect" echo " 2. Access your NAS via SSH on port ${SSH_PORT:-$DEFAULT_SSH_PORT}" echo " 3. Configure file shares through Samba" echo " 4. Review firewall rules with: sudo ufw status" echo " 5. Check the services summary file: cat ${summary_file}" echo log_warning "Important: Please save the following information:" echo " - SSH Port: ${SSH_PORT:-$DEFAULT_SSH_PORT}" echo " - Admin User: ${ADMIN_USER:-$NEW_USER}" echo " - Configuration saved in: ${CONFIG_FILE}" echo " - Installation log: ${LOG_FILE}" echo " - Services summary: ${summary_file}" } # Main script execution main() { clear echo -e "${CYAN}${BOLD}" echo "╔══════════════════════════════════════════════════════════════╗" echo "║ ║" echo "║ ${SCRIPT_NAME} v${SCRIPT_VERSION} ║" echo "║ ║" echo "║ Automated NAS Setup for Multiple Linux Distros ║" echo "║ ║" echo "║ by ${SCRIPT_AUTHOR} ║" echo "║ ║" echo "╚══════════════════════════════════════════════════════════════╝" echo -e "${NC}" echo log_info "${SCRIPT_NAME} v${SCRIPT_VERSION} started" log_info "Running on: $(uname -a)" # Pre-flight checks detect_distro check_system_requirements get_system_info # Unset any existing config variables to ensure clean state unset INSTALL_DOCKER INSTALL_NFS INSTALL_NETDATA INSTALL_VAULTWARDEN INSTALL_JELLYFIN INSTALL_PORTAINER INSTALL_WEBMIN CONFIGURE_STATIC_IP SSH_PORT ADMIN_USER # Configuration load_or_create_config # Confirmation echo if ! ask_yes_no "Ready to start installation?" "y"; then log_info "Installation cancelled by user" exit 0 fi # Main installation log_info "Starting installation process..." preflight_apt_cleanup update_system run_installation # Cleanup and summary cleanup optimize_nas_performance perform_health_check show_installation_summary # Reboot prompt echo if ask_yes_no "Reboot system now to complete setup?" "y"; then log_info "System will reboot in 5 seconds..." sleep 5 sudo reboot else log_warning "Please reboot the system manually to complete the setup" fi } # Run main function main "$@"