Files
nas/lib/common.sh

506 lines
14 KiB
Bash

#!/bin/bash
# Common functions and input validation
# Input validation functions
validate_ip() {
local ip=$1
if [[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
IFS='.' read -ra ADDR <<< "$ip"
for i in "${ADDR[@]}"; do
if [[ $i -gt 255 ]]; then
return 1
fi
done
return 0
else
return 1
fi
}
validate_port() {
local port=$1
if [[ $port =~ ^[0-9]+$ ]] && [[ $port -ge 1 ]] && [[ $port -le 65535 ]]; then
return 0
else
return 1
fi
}
validate_username() {
local username=$1
if [[ $username =~ ^[a-z_][a-z0-9_-]{0,31}$ ]]; then
return 0
else
return 1
fi
}
validate_path() {
local path=$1
if [[ $path =~ ^/[a-zA-Z0-9_/.-]*$ ]]; then
return 0
else
return 1
fi
}
# Enhanced user input functions
ask_yes_no() {
local prompt="$1"
local default="${2:-n}"
local response
while true; do
if [[ $default == "y" ]]; then
read -p "$prompt [Y/n]: " response
response=${response:-y}
else
read -p "$prompt [y/N]: " response
response=${response:-n}
fi
case "${response,,}" in
y|yes) return 0 ;;
n|no) return 1 ;;
*) log_warning "Please answer yes (y) or no (n)." ;;
esac
done
}
ask_input() {
local prompt="$1"
local default="$2"
local validator="$3"
local response
while true; do
if [[ -n "$default" ]]; then
read -p "$prompt [$default]: " response
response=${response:-$default}
else
read -p "$prompt: " response
fi
if [[ -z "$response" && -z "$default" ]]; then
log_warning "Input cannot be empty."
continue
fi
if [[ -n "$validator" ]]; then
if $validator "$response"; then
echo "$response"
return 0
else
log_warning "Invalid input. Please try again."
continue
fi
else
echo "$response"
return 0
fi
done
}
ask_password() {
local prompt="$1"
local password
local password_confirm
while true; do
read -s -p "$prompt: " password
echo
if [[ ${#password} -lt 8 ]]; then
log_warning "Password must be at least 8 characters long."
continue
fi
read -s -p "Confirm password: " password_confirm
echo
if [[ "$password" == "$password_confirm" ]]; then
echo "$password"
return 0
else
log_warning "Passwords do not match. Please try again."
fi
done
}
# System checks
check_root() {
if [[ $EUID -ne 0 ]]; then
log_error "This script must be run as root or with sudo privileges."
exit 1
fi
}
check_disk_space() {
local required_gb=${1:-20}
local available_gb=$(df / | tail -1 | awk '{printf "%.0f", $4/1024/1024}')
if [[ $available_gb -lt $required_gb ]]; then
log_error "Insufficient disk space. Required: ${required_gb}GB, Available: ${available_gb}GB"
return 1
else
log_info "Disk space check passed. Available: ${available_gb}GB"
return 0
fi
}
check_ram() {
local required_mb=${1:-2048}
local available_mb=$(free -m | awk 'NR==2{printf "%.0f", $2}')
if [[ $available_mb -lt $required_mb ]]; then
log_warning "Low RAM detected. Required: ${required_mb}MB, Available: ${available_mb}MB"
if ! ask_yes_no "Continue anyway?" "n"; then
exit 1
fi
else
log_info "RAM check passed. Available: ${available_mb}MB"
fi
}
check_internet_enhanced() {
local test_hosts=("8.8.8.8" "1.1.1.1" "google.com")
local success=false
log_info "Testing internet connectivity..."
for host in "${test_hosts[@]}"; do
if ping -c 1 -W 5 "$host" &>/dev/null; then
log_success "Internet connectivity confirmed (via $host)"
success=true
break
fi
done
if [[ "$success" == false ]]; then
log_error "No internet connection detected. Please check your network settings."
return 1
fi
return 0
}
# Dependency checks
check_command() {
local cmd="$1"
if command -v "$cmd" &>/dev/null; then
log_debug "Command '$cmd' is available"
return 0
else
log_error "Required command '$cmd' is not available"
return 1
fi
}
install_dependencies() {
local dependencies=("curl" "wget" "git" "ufw" "htop" "tree" "bc")
local missing_deps=()
log_info "Checking system dependencies..."
for dep in "${dependencies[@]}"; do
if ! command -v "$dep" &>/dev/null; then
missing_deps+=("$dep")
fi
done
if [[ ${#missing_deps[@]} -gt 0 ]]; then
log_info "Installing missing dependencies: ${missing_deps[*]}"
case $DISTRO in
ubuntu|debian)
sudo apt-get update
sudo apt-get install -y "${missing_deps[@]}"
;;
fedora)
sudo dnf install -y "${missing_deps[@]}"
;;
arch)
sudo pacman -S --noconfirm "${missing_deps[@]}"
;;
opensuse)
sudo zypper install -y "${missing_deps[@]}"
;;
esac
else
log_success "All dependencies are already installed"
fi
}
# Service management
service_exists() {
local service="$1"
systemctl list-unit-files --type=service | grep -q "^${service}.service"
}
is_service_running() {
local service="$1"
systemctl is-active --quiet "$service"
}
start_and_enable_service() {
local service="$1"
if service_exists "$service"; then
if systemctl enable "$service" && systemctl start "$service"; then
log_success "Service '$service' started and enabled"
add_rollback_action "systemctl stop $service && systemctl disable $service"
return 0
else
log_error "Failed to start service '$service'"
return 1
fi
else
log_error "Service '$service' does not exist"
return 1
fi
}
# Robust SSH restart helper: try sshd, then ssh, then service command
restart_ssh_service() {
log_info "Attempting to restart SSH service using available service name..."
if systemctl list-unit-files --type=service | grep -q "^sshd.service"; then
if sudo systemctl restart sshd; then
log_success "sshd.service restarted successfully"
return 0
else
log_warning "Failed to restart sshd.service"
fi
fi
if systemctl list-unit-files --type=service | grep -q "^ssh.service"; then
if sudo systemctl restart ssh; then
log_success "ssh.service restarted successfully"
return 0
else
log_warning "Failed to restart ssh.service"
fi
fi
# Fallback to service command
if command -v service >/dev/null 2>&1; then
if sudo service ssh restart; then
log_success "SSH restarted via service ssh restart"
return 0
else
log_warning "Failed to restart SSH via 'service ssh restart'"
fi
fi
log_error "Unable to restart SSH service with known methods"
return 1
}
# Configuration management
save_config() {
local key="$1"
local value="$2"
if [[ -f "${CONFIG_FILE}" ]]; then
if grep -q "^${key}=" "${CONFIG_FILE}"; then
# Use sed with proper escaping
sed -i.bak "s|^${key}=.*|${key}=${value}|" "${CONFIG_FILE}" && rm -f "${CONFIG_FILE}.bak"
else
echo "${key}=${value}" >> "${CONFIG_FILE}"
fi
else
echo "${key}=${value}" > "${CONFIG_FILE}"
fi
log_debug "Saved config: ${key}=${value}"
}
load_config() {
if [[ -f "${CONFIG_FILE}" ]]; then
source "${CONFIG_FILE}"
log_debug "Configuration loaded from ${CONFIG_FILE}"
return 0
else
log_debug "No configuration file found at ${CONFIG_FILE}"
return 1
fi
}
# Cleanup function
cleanup() {
log_info "Performing cleanup..."
# Remove temporary files
rm -f /tmp/nas_setup_*
# Clear package cache based on distro
case $DISTRO in
ubuntu|debian)
sudo apt-get autoremove -y
sudo apt-get autoclean
;;
fedora)
sudo dnf autoremove -y
sudo dnf clean all
;;
arch)
sudo pacman -Sc --noconfirm
;;
opensuse)
sudo zypper clean -a
;;
esac
log_success "Cleanup completed"
}
# Performance monitoring
get_system_info() {
log_info "System Information:"
echo " OS: $(lsb_release -d | cut -f2)"
echo " Kernel: $(uname -r)"
echo " CPU: $(lscpu | grep 'Model name' | cut -d: -f2 | xargs)"
echo " RAM: $(free -h | awk 'NR==2{printf "%s/%s", $3,$2}')"
echo " Disk: $(df -h / | awk 'NR==2{printf "%s/%s (%s used)", $3,$2,$5}')"
echo " Uptime: $(uptime -p)"
}
# Version and compatibility checks
check_ubuntu_version() {
if [[ "$DISTRO" == "ubuntu" ]]; then
local version_major=$(echo "$DISTRO_VERSION" | cut -d'.' -f1)
if [[ $version_major -lt 24 ]]; then
log_warning "Ubuntu version $DISTRO_VERSION is not officially supported. Minimum: 24.04"
if ! ask_yes_no "Continue anyway?" "n"; then
exit 1
fi
else
log_success "Ubuntu version $DISTRO_VERSION is supported"
fi
fi
}
# Configure automatic updates
configure_automatic_updates() {
log_info "Configuring automatic security updates..."
case $DISTRO in
ubuntu|debian)
sudo apt-get install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
# Configure unattended-upgrades
sudo tee /etc/apt/apt.conf.d/50unattended-upgrades > /dev/null <<EOF
Unattended-Upgrade::Allowed-Origins {
"\${distro_id}:\${distro_codename}-security";
"\${distro_id}ESMApps:\${distro_codename}-apps-security";
"\${distro_id}ESM:\${distro_codename}-infra-security";
};
Unattended-Upgrade::AutoFixInterruptedDpkg "true";
Unattended-Upgrade::MinimalSteps "true";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Automatic-Reboot "false";
EOF
;;
fedora)
sudo dnf install -y dnf-automatic
sudo systemctl enable --now dnf-automatic-install.timer
;;
arch)
log_info "Arch Linux uses rolling releases - manual updates recommended"
;;
opensuse)
sudo zypper install -y yast2-online-update-configuration
;;
esac
log_success "Automatic updates configured"
}
# Setup basic monitoring tools
setup_basic_monitoring() {
log_info "Installing basic monitoring tools..."
case $DISTRO in
ubuntu|debian)
sudo apt-get install -y htop iotop ncdu tree
;;
fedora)
sudo dnf install -y htop iotop ncdu tree
;;
arch)
sudo pacman -S --noconfirm htop iotop ncdu tree
;;
opensuse)
sudo zypper install -y htop iotop ncdu tree
;;
esac
log_success "Basic monitoring tools installed"
}
# Preflight: check for running apt/dpkg processes and leftover locks, attempt safe cleanup
preflight_apt_cleanup() {
log_info "Checking for running apt/dpkg processes and lock files..."
# List processes matching common package manager names
local procs
procs=$(pgrep -a -f "apt-get|apt|dpkg|unattended-upgrade|aptitude" 2>/dev/null || true)
if [[ -n "$procs" ]]; then
log_warning "Found running package processes:"
echo "$procs"
log_info "Waiting up to 15s for them to finish gracefully..."
for i in {1..15}; do
sleep 1
if ! pgrep -f "apt-get|apt|dpkg|unattended-upgrade|aptitude" >/dev/null; then
break
fi
done
fi
if pgrep -f "apt-get|apt|dpkg|unattended-upgrade|aptitude" >/dev/null; then
log_warning "Package processes still running; attempting graceful stop..."
sudo pkill -15 -f "apt-get|apt|unattended-upgrade|aptitude" || true
sleep 2
if pgrep -f "apt-get|apt|dpkg|unattended-upgrade|aptitude" >/dev/null; then
log_warning "Forcing kill of remaining package processes..."
sudo pkill -9 -f "apt-get|apt|dpkg|unattended-upgrade|aptitude" || true
fi
fi
# Remove common lock files if present (safe to remove if processes are gone)
sudo rm -f /var/lib/dpkg/lock-frontend /var/lib/dpkg/lock /var/cache/apt/archives/lock /var/lib/apt/lists/lock || true
# Try to finish interrupted package configuration
log_info "Running 'dpkg --configure -a' and 'apt-get -f install' (non-interactive) to fix package state..."
sudo dpkg --configure -a || true
sudo env DEBIAN_FRONTEND=noninteractive APT_LISTCHANGES_FRONTEND=none apt-get -y -f install || true
log_success "Apt/dpkg preflight cleanup completed"
}
# Install additional components
install_additional_components() {
log_info "Installing additional useful components..."
local packages=("curl" "wget" "git" "vim" "nano" "screen" "tmux" "rsync" "zip" "unzip")
case $DISTRO in
ubuntu|debian)
sudo apt-get install -y "${packages[@]}"
;;
fedora)
sudo dnf install -y "${packages[@]}"
;;
arch)
sudo pacman -S --noconfirm "${packages[@]}"
;;
opensuse)
sudo zypper install -y "${packages[@]}"
;;
esac
log_success "Additional components installed"
}