feat(windows): add windows compatible
This commit is contained in:
Executable
+651
@@ -0,0 +1,651 @@
|
||||
#!/bin/bash
|
||||
|
||||
# LXS - CloudPanel Installation Script
|
||||
# Description: Install and manage CloudPanel hosting platform
|
||||
# Author: LXS
|
||||
# Date: 2025
|
||||
|
||||
# Load LXS common library (colors, separator, run_spinner, loggers)
|
||||
LXS_RAW_BASE="${LXS_RAW_BASE:-https://git.hyko.cx/hykocx/lxs/raw/branch/main}"
|
||||
_lib=$(curl -fsSL "${LXS_RAW_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
|
||||
eval "$_lib"
|
||||
unset _lib
|
||||
export LXS_LOG_FILE="/tmp/lxs_cloudpanel.log"
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Configuration
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
INSTALL_DIR="/usr/local/cloudpanel"
|
||||
PANEL_PORT=8443
|
||||
INSTALLER_URL="https://installer.cloudpanel.io/ce/v2/install.sh"
|
||||
INSTALLER_SHA256="19cfa702e7936a79e47812ff57d9859175ea902c62a68b2c15ccd1ebaf36caeb"
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Helper Functions
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
is_installed() {
|
||||
[ -d "$INSTALL_DIR" ] || command -v clpctl &> /dev/null || [ -f "/usr/local/bin/clpctl" ]
|
||||
}
|
||||
|
||||
check_requirements() {
|
||||
echo -e "${WHITE}Checking System Requirements...${NC}"
|
||||
|
||||
# Check root access
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo -e "${RED}[✗] This script must be run as root${NC}"
|
||||
return 1
|
||||
fi
|
||||
echo -e "${GREEN}[✓] Running as root${NC}"
|
||||
|
||||
local cpu_cores=$(nproc)
|
||||
[ $cpu_cores -lt 1 ] && echo -e "${RED}[✗] Insufficient CPU cores (minimum: 1)${NC}" && return 1
|
||||
echo -e "${GREEN}[✓] CPU cores: $cpu_cores${NC}"
|
||||
|
||||
local total_mem_gb=$(($(grep MemTotal /proc/meminfo | awk '{print $2}') / 1024 / 1024))
|
||||
if [ $total_mem_gb -lt 1 ]; then
|
||||
echo -e "${RED}[✗] Insufficient RAM: ${total_mem_gb}GB (minimum: 1GB)${NC}"
|
||||
return 1
|
||||
elif [ $total_mem_gb -lt 2 ]; then
|
||||
echo -e "${YELLOW}[!] Warning: 2GB RAM is recommended (found: ${total_mem_gb}GB)${NC}"
|
||||
else
|
||||
echo -e "${GREEN}[✓] Memory: ${total_mem_gb}GB${NC}"
|
||||
fi
|
||||
|
||||
local available_gb=$(($(df / | awk 'NR==2 {print $4}') / 1024 / 1024))
|
||||
if [ $available_gb -lt 10 ]; then
|
||||
echo -e "${RED}[✗] Insufficient disk space: ${available_gb}GB (minimum: 10GB)${NC}"
|
||||
return 1
|
||||
elif [ $available_gb -lt 20 ]; then
|
||||
echo -e "${YELLOW}[!] Warning: 20GB free space is recommended (found: ${available_gb}GB)${NC}"
|
||||
else
|
||||
echo -e "${GREEN}[✓] Disk space: ${available_gb}GB available${NC}"
|
||||
fi
|
||||
|
||||
local arch=$(uname -m)
|
||||
if [[ "$arch" != "x86_64" && "$arch" != "aarch64" ]]; then
|
||||
echo -e "${RED}[✗] Unsupported architecture: $arch${NC}"
|
||||
return 1
|
||||
fi
|
||||
echo -e "${GREEN}[✓] Architecture: $arch${NC}"
|
||||
|
||||
# Check OS - CloudPanel supports Ubuntu 24.04, 22.04, Debian 12, 11
|
||||
if [ -f /etc/os-release ]; then
|
||||
. /etc/os-release
|
||||
case "$ID" in
|
||||
ubuntu)
|
||||
if [[ "$VERSION_ID" == "24.04" || "$VERSION_ID" == "22.04" ]]; then
|
||||
echo -e "${GREEN}[✓] OS: $PRETTY_NAME${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}[!] Warning: Ubuntu version $VERSION_ID may not be officially supported${NC}"
|
||||
echo -e "${GRAY} Supported: Ubuntu 24.04 LTS, Ubuntu 22.04 LTS${NC}"
|
||||
fi
|
||||
;;
|
||||
debian)
|
||||
if [[ "$VERSION_ID" == "12" || "$VERSION_ID" == "11" ]]; then
|
||||
echo -e "${GREEN}[✓] OS: $PRETTY_NAME${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}[!] Warning: Debian version $VERSION_ID may not be officially supported${NC}"
|
||||
echo -e "${GRAY} Supported: Debian 12 LTS, Debian 11 LTS${NC}"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}[✗] Unsupported OS: $PRETTY_NAME${NC}"
|
||||
echo -e "${GRAY} Supported: Ubuntu 24.04/22.04, Debian 12/11${NC}"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
else
|
||||
echo -e "${YELLOW}[!] Warning: Cannot detect OS version${NC}"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
get_clpctl_path() {
|
||||
local max_attempts=30
|
||||
local attempt=0
|
||||
local clpctl_path=""
|
||||
|
||||
while [ $attempt -lt $max_attempts ]; do
|
||||
if command -v clpctl &> /dev/null; then
|
||||
clpctl_path="clpctl"
|
||||
break
|
||||
elif [ -f "/usr/local/bin/clpctl" ]; then
|
||||
clpctl_path="/usr/local/bin/clpctl"
|
||||
break
|
||||
fi
|
||||
|
||||
attempt=$((attempt + 1))
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "$clpctl_path"
|
||||
}
|
||||
|
||||
configure_cloudpanel_basic_auth() {
|
||||
echo ""
|
||||
echo -e "${WHITE}${BOLD}Configuring CloudPanel Basic Auth...${NC}\n"
|
||||
|
||||
# Wait for clpctl to be available
|
||||
echo -e "${PURPLE}[*] Waiting for CloudPanel CLI to be ready...${NC}"
|
||||
local clpctl_path=$(get_clpctl_path)
|
||||
|
||||
if [ -z "$clpctl_path" ]; then
|
||||
echo -e "${YELLOW}[!] Warning: CloudPanel CLI not found, skipping Basic Auth configuration${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}[✓] CloudPanel CLI is ready${NC}"
|
||||
echo ""
|
||||
|
||||
# Generate random credentials (global variables for display later)
|
||||
BASIC_AUTH_USERNAME=$(generate_password 8)
|
||||
BASIC_AUTH_PASSWORD=$(generate_password 20)
|
||||
|
||||
echo -e "${PURPLE}[*] Enabling Basic Auth...${NC}"
|
||||
|
||||
# Enable Basic Auth
|
||||
if $clpctl_path cloudpanel:enable:basic-auth --userName="$BASIC_AUTH_USERNAME" --password="$BASIC_AUTH_PASSWORD" >> /tmp/lxs_cloudpanel.log 2>&1; then
|
||||
echo -e "${GREEN}[✓] Basic Auth enabled successfully${NC}"
|
||||
echo ""
|
||||
return 0
|
||||
else
|
||||
echo -e "${YELLOW}[!] Warning: Failed to enable Basic Auth${NC}"
|
||||
echo -e "${GRAY} You can enable it manually later with:${NC}"
|
||||
echo -e "${GRAY} clpctl cloudpanel:enable:basic-auth --userName=USERNAME --password='PASSWORD'${NC}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
create_cloudpanel_admin() {
|
||||
echo ""
|
||||
echo -e "${WHITE}${BOLD}Creating CloudPanel Admin Account...${NC}\n"
|
||||
|
||||
# Wait for clpctl to be available
|
||||
echo -e "${PURPLE}[*] Waiting for CloudPanel CLI to be ready...${NC}"
|
||||
local clpctl_path=$(get_clpctl_path)
|
||||
|
||||
if [ -z "$clpctl_path" ]; then
|
||||
echo -e "${YELLOW}[!] Warning: CloudPanel CLI not found, skipping admin account creation${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}[✓] CloudPanel CLI is ready${NC}"
|
||||
echo ""
|
||||
|
||||
# Ask for admin email
|
||||
echo -e "${CYAN}Admin Email (ex. [email protected]):${NC}"
|
||||
read -r ADMIN_EMAIL
|
||||
while [ -z "$ADMIN_EMAIL" ]; do
|
||||
echo -e "${RED}Email is required!${NC}"
|
||||
read -r ADMIN_EMAIL
|
||||
done
|
||||
echo ""
|
||||
|
||||
# Generate random credentials (global variables for display later)
|
||||
ADMIN_USERNAME=$(generate_password 8 | tr '[:upper:]' '[:lower:]')
|
||||
ADMIN_PASSWORD=$(generate_password 20)
|
||||
|
||||
echo -e "${PURPLE}[*] Creating admin account...${NC}"
|
||||
|
||||
# Create admin user
|
||||
if $clpctl_path user:add --userName="$ADMIN_USERNAME" --email="$ADMIN_EMAIL" --firstName="Admin" --lastName="User" --password="$ADMIN_PASSWORD" --role="admin" --timezone="UTC" --status="1" >> /tmp/lxs_cloudpanel.log 2>&1; then
|
||||
echo -e "${GREEN}[✓] Admin account created successfully${NC}"
|
||||
echo ""
|
||||
return 0
|
||||
else
|
||||
echo -e "${YELLOW}[!] Warning: Failed to create admin account${NC}"
|
||||
echo -e "${GRAY} You can create it manually later with:${NC}"
|
||||
echo -e "${GRAY} clpctl user:add --userName=$ADMIN_USERNAME --email=$ADMIN_EMAIL --firstName='Admin' --lastName='User' --password='$ADMIN_PASSWORD' --role='admin' --timezone='UTC' --status='1'${NC}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Installation Functions
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
install_cloudpanel() {
|
||||
local start_time=$(date +%s)
|
||||
|
||||
apt_noninteractive
|
||||
|
||||
echo -e "${WHITE}${BOLD}CLOUDPANEL INSTALLATION${NC}\n"
|
||||
|
||||
if is_installed; then
|
||||
echo -e "${YELLOW}CloudPanel is already installed!${NC}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
require_disk_space 2048 || return 1
|
||||
check_requirements || return 1
|
||||
echo ""
|
||||
|
||||
wait_for_apt || return 1
|
||||
echo ""
|
||||
|
||||
# Update system and install required packages
|
||||
echo -e "${PURPLE}[*] Updating system packages...${NC}"
|
||||
if ! run_spinner "Updating system" apt update && apt -y upgrade && apt -y install curl wget sudo; then
|
||||
echo -e "${RED}[✗] Failed to update system packages${NC}"
|
||||
return 1
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Set database engine to MySQL 8.4
|
||||
DB_ENGINE="MYSQL_8.4"
|
||||
echo -e "${GREEN}[✓] Database engine: MySQL 8.4${NC}"
|
||||
echo ""
|
||||
|
||||
# Download installer
|
||||
echo -e "${PURPLE}[*] Downloading CloudPanel installer...${NC}"
|
||||
if ! curl -sS "$INSTALLER_URL" -o /tmp/cloudpanel_install.sh; then
|
||||
echo -e "${RED}[✗] Failed to download installer${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Verify SHA256 checksum
|
||||
echo -e "${PURPLE}[*] Verifying installer checksum...${NC}"
|
||||
local calculated_sha=$(sha256sum /tmp/cloudpanel_install.sh | awk '{print $1}')
|
||||
if [ "$calculated_sha" != "$INSTALLER_SHA256" ]; then
|
||||
echo -e "${RED}[✗] Installer checksum verification failed!${NC}"
|
||||
echo -e "${GRAY} Expected: $INSTALLER_SHA256${NC}"
|
||||
echo -e "${GRAY} Got: $calculated_sha${NC}"
|
||||
rm -f /tmp/cloudpanel_install.sh
|
||||
return 1
|
||||
fi
|
||||
echo -e "${GREEN}[✓] Installer checksum verified${NC}"
|
||||
echo ""
|
||||
|
||||
# Get server IP
|
||||
local server_ip=$(get_public_ip)
|
||||
|
||||
echo -e "${WHITE}Starting Installation...${NC}"
|
||||
echo -e "${GRAY}[i] This may take 10-20 minutes...${NC}"
|
||||
echo ""
|
||||
|
||||
# Execute installation
|
||||
show_separator
|
||||
DB_ENGINE=$DB_ENGINE bash /tmp/cloudpanel_install.sh
|
||||
local install_exit_code=$?
|
||||
show_separator
|
||||
|
||||
# Clean up installer
|
||||
rm -f /tmp/cloudpanel_install.sh
|
||||
|
||||
if [ $install_exit_code -ne 0 ]; then
|
||||
echo ""
|
||||
echo -e "${RED}[✗] Installation failed!${NC}"
|
||||
echo -e "${GRAY}Check /tmp/lxs_cloudpanel.log for details${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}[✓] Installation completed${NC}"
|
||||
|
||||
ufw_allow 20/tcp "CloudPanel FTP data"
|
||||
ufw_allow 21/tcp "CloudPanel FTP"
|
||||
ufw_allow 25/tcp "CloudPanel SMTP"
|
||||
ufw_allow 53/tcp "CloudPanel DNS (TCP)"
|
||||
ufw_allow 53/udp "CloudPanel DNS (UDP)"
|
||||
ufw_allow 80/tcp "CloudPanel HTTP"
|
||||
ufw_allow 443/tcp "CloudPanel HTTPS"
|
||||
ufw_allow 465/tcp "CloudPanel SMTPS"
|
||||
ufw_allow 587/tcp "CloudPanel submission"
|
||||
ufw_allow 993/tcp "CloudPanel IMAPS"
|
||||
ufw_allow 995/tcp "CloudPanel POP3S"
|
||||
ufw_allow "${PANEL_PORT}/tcp" "CloudPanel admin"
|
||||
|
||||
# Configure Basic Auth
|
||||
configure_cloudpanel_basic_auth
|
||||
|
||||
# Create admin account
|
||||
create_cloudpanel_admin
|
||||
|
||||
local duration=$(( $(date +%s) - start_time ))
|
||||
local minutes=$((duration / 60))
|
||||
local seconds=$((duration % 60))
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}${BOLD}Installation Completed Successfully!${NC}"
|
||||
echo -e "${GRAY}Installation time: ${minutes}m ${seconds}s${NC}"
|
||||
echo ""
|
||||
show_separator
|
||||
echo -e "${WHITE}${BOLD}CLOUDPANEL ACCESS INFORMATION${NC}"
|
||||
show_separator
|
||||
echo -e "${WHITE}Panel URL: ${GREEN}${BOLD}https://$server_ip:$PANEL_PORT${NC}"
|
||||
|
||||
# Display Basic Auth credentials if configured
|
||||
if [ -n "$BASIC_AUTH_USERNAME" ] && [ -n "$BASIC_AUTH_PASSWORD" ]; then
|
||||
echo ""
|
||||
echo -e "${WHITE}${BOLD}BASIC AUTH CREDENTIALS${NC}"
|
||||
echo -e "${WHITE}Username: ${GREEN}${BOLD}$BASIC_AUTH_USERNAME${NC}"
|
||||
echo -e "${WHITE}Password: ${GREEN}${BOLD}$BASIC_AUTH_PASSWORD${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}[!] Basic Auth is enabled. You will need these credentials to access CloudPanel.${NC}"
|
||||
fi
|
||||
|
||||
# Display Admin account credentials if created
|
||||
if [ -n "$ADMIN_USERNAME" ] && [ -n "$ADMIN_PASSWORD" ]; then
|
||||
echo ""
|
||||
echo -e "${WHITE}${BOLD}ADMIN ACCOUNT CREDENTIALS${NC}"
|
||||
echo -e "${WHITE}Email: ${GREEN}${BOLD}$ADMIN_EMAIL${NC}"
|
||||
echo -e "${WHITE}Username: ${GREEN}${BOLD}$ADMIN_USERNAME${NC}"
|
||||
echo -e "${WHITE}Password: ${GREEN}${BOLD}$ADMIN_PASSWORD${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${RED}${BOLD}IMPORTANT SECURITY STEPS:${NC}"
|
||||
echo -e "${YELLOW}1. Access CloudPanel immediately via the URL above${NC}"
|
||||
echo -e "${YELLOW}2. Ignore the self-signed certificate warning${NC}"
|
||||
echo -e "${YELLOW}3. Click 'Advanced' and 'Proceed' to continue${NC}"
|
||||
if [ -n "$BASIC_AUTH_USERNAME" ] && [ -n "$BASIC_AUTH_PASSWORD" ]; then
|
||||
echo -e "${YELLOW}4. Enter Basic Auth credentials when prompted${NC}"
|
||||
fi
|
||||
if [ -n "$ADMIN_USERNAME" ] && [ -n "$ADMIN_PASSWORD" ]; then
|
||||
echo -e "${YELLOW}$([ -n "$BASIC_AUTH_USERNAME" ] && echo "5" || echo "4"). Login with admin credentials above${NC}"
|
||||
fi
|
||||
echo ""
|
||||
echo -e "${RED}${BOLD}REQUIRED FIREWALL PORTS:${NC}"
|
||||
echo -e "${GRAY}20, 21, 22, 25, 53, 80, 443, 465, 587, 993, 995, $PANEL_PORT${NC}"
|
||||
show_separator
|
||||
|
||||
if [ -n "$BASIC_AUTH_USERNAME" ] && [ -n "$BASIC_AUTH_PASSWORD" ]; then
|
||||
echo ""
|
||||
echo -e "${RED}${BOLD}IMPORTANT:${NC} ${YELLOW}Save Basic Auth credentials securely!${NC}"
|
||||
fi
|
||||
if [ -n "$ADMIN_USERNAME" ] && [ -n "$ADMIN_PASSWORD" ]; then
|
||||
echo ""
|
||||
echo -e "${RED}${BOLD}IMPORTANT:${NC} ${YELLOW}Save admin credentials securely!${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
update_cloudpanel() {
|
||||
echo -e "${WHITE}${BOLD}CLOUDPANEL UPDATE${NC}\n"
|
||||
|
||||
apt_noninteractive
|
||||
|
||||
if ! is_installed; then
|
||||
echo -e "${RED}CloudPanel is not installed!${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo -e "${PURPLE}[*] Updating CloudPanel...${NC}"
|
||||
|
||||
# Use clpctl if available
|
||||
if command -v clpctl &> /dev/null; then
|
||||
clpctl update
|
||||
elif [ -f "/usr/local/bin/clpctl" ]; then
|
||||
/usr/local/bin/clpctl update
|
||||
else
|
||||
echo -e "${YELLOW}[!] CloudPanel CLI not found, downloading latest installer...${NC}"
|
||||
curl -sS "$INSTALLER_URL" -o /tmp/cloudpanel_update.sh
|
||||
if echo "$INSTALLER_SHA256 /tmp/cloudpanel_update.sh" | sha256sum -c --quiet; then
|
||||
bash /tmp/cloudpanel_update.sh
|
||||
rm -f /tmp/cloudpanel_update.sh
|
||||
else
|
||||
echo -e "${RED}[✗] Failed to verify update installer${NC}"
|
||||
rm -f /tmp/cloudpanel_update.sh
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
[ $? -eq 0 ] && echo -e "${GREEN}[✓] Update completed${NC}" || echo -e "${RED}[✗] Update failed${NC}"
|
||||
}
|
||||
|
||||
show_status() {
|
||||
echo -e "${WHITE}${BOLD}CLOUDPANEL STATUS${NC}\n"
|
||||
|
||||
if ! is_installed; then
|
||||
echo -e "${RED}CloudPanel is not installed!${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check CloudPanel service
|
||||
if systemctl is-active --quiet cloudpanel; then
|
||||
echo -e "${GREEN}[✓] CloudPanel service is running${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}[!] CloudPanel service status unknown${NC}"
|
||||
fi
|
||||
|
||||
# Check web server
|
||||
if systemctl is-active --quiet nginx || systemctl is-active --quiet apache2; then
|
||||
echo -e "${GREEN}[✓] Web server is running${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}[!] Web server status unknown${NC}"
|
||||
fi
|
||||
|
||||
# Check database
|
||||
if systemctl is-active --quiet mysql || systemctl is-active --quiet mariadb; then
|
||||
echo -e "${GREEN}[✓] Database service is running${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}[!] Database service status unknown${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
local server_ip=$(get_public_ip)
|
||||
echo -e "${CYAN}Panel URL: ${BOLD}https://$server_ip:$PANEL_PORT${NC}"
|
||||
|
||||
# Show clpctl status if available
|
||||
if command -v clpctl &> /dev/null || [ -f "/usr/local/bin/clpctl" ]; then
|
||||
echo ""
|
||||
echo -e "${WHITE}CloudPanel CLI available${NC}"
|
||||
echo -e "${GRAY}Run 'clpctl' for available commands${NC}"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
install_rclone() {
|
||||
echo -e "${PURPLE}[*] Checking rclone installation...${NC}"
|
||||
|
||||
if command -v rclone &> /dev/null; then
|
||||
local rclone_version=$(rclone version | head -n 1 | awk '{print $2}')
|
||||
echo -e "${GREEN}[✓] rclone is already installed (version: $rclone_version)${NC}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo -e "${PURPLE}[*] Installing rclone...${NC}"
|
||||
|
||||
wait_for_apt || return 1
|
||||
|
||||
# Install rclone using official method
|
||||
if ! run_spinner "Installing rclone" curl https://rclone.org/install.sh | bash; then
|
||||
echo -e "${RED}[✗] Failed to install rclone${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if command -v rclone &> /dev/null; then
|
||||
local rclone_version=$(rclone version | head -n 1 | awk '{print $2}')
|
||||
echo -e "${GREEN}[✓] rclone installed successfully (version: $rclone_version)${NC}"
|
||||
return 0
|
||||
else
|
||||
echo -e "${RED}[✗] rclone installation failed${NC}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
configure_rclone_backblaze() {
|
||||
echo ""
|
||||
echo -e "${WHITE}${BOLD}Configure rclone for Backblaze B2${NC}\n"
|
||||
|
||||
# Check if CloudPanel is installed
|
||||
if ! is_installed; then
|
||||
echo -e "${YELLOW}[!] CloudPanel is not installed. Please install CloudPanel first.${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Install rclone if needed
|
||||
if ! install_rclone; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${CYAN}Backblaze B2 Configuration${NC}"
|
||||
echo -e "${GRAY}You need to provide your Backblaze B2 credentials.${NC}"
|
||||
echo -e "${GRAY}To get your credentials:${NC}"
|
||||
echo -e "${GRAY} 1. Log in to your Backblaze account${NC}"
|
||||
echo -e "${GRAY} 2. Go to 'App Keys' section${NC}"
|
||||
echo -e "${GRAY} 3. Create a new Application Key (or use an existing one)${NC}"
|
||||
echo -e "${GRAY} 4. Copy the 'Key ID' (Account ID) and 'Application Key'${NC}"
|
||||
echo ""
|
||||
|
||||
# Ask for Account ID (Key ID)
|
||||
echo -e "${CYAN}Account ID (Key ID):${NC}"
|
||||
read -r B2_ACCOUNT_ID
|
||||
while [ -z "$B2_ACCOUNT_ID" ]; do
|
||||
echo -e "${RED}Account ID is required!${NC}"
|
||||
read -r B2_ACCOUNT_ID
|
||||
done
|
||||
|
||||
# Ask for Application Key
|
||||
echo ""
|
||||
echo -e "${CYAN}Application Key:${NC}"
|
||||
read -rs B2_APPLICATION_KEY
|
||||
while [ -z "$B2_APPLICATION_KEY" ]; do
|
||||
echo -e "${RED}Application Key is required!${NC}"
|
||||
read -rs B2_APPLICATION_KEY
|
||||
done
|
||||
echo ""
|
||||
|
||||
# Ask for bucket name (optional, for information)
|
||||
echo ""
|
||||
echo -e "${CYAN}Bucket Name (optional, for reference):${NC}"
|
||||
read -r B2_BUCKET_NAME
|
||||
|
||||
echo ""
|
||||
echo -e "${PURPLE}[*] Configuring rclone...${NC}"
|
||||
|
||||
# Create rclone config directory if it doesn't exist
|
||||
# Use root's home directory since script runs as root
|
||||
local rclone_config_dir="/root/.config/rclone"
|
||||
mkdir -p "$rclone_config_dir"
|
||||
|
||||
# Configure rclone non-interactively
|
||||
# The remote name must be "remote" for CloudPanel to recognize it
|
||||
local rclone_config_file="$rclone_config_dir/rclone.conf"
|
||||
|
||||
# Check if remote already exists
|
||||
if [ -f "$rclone_config_file" ] && grep -q "^\[remote\]" "$rclone_config_file" 2>/dev/null; then
|
||||
echo -e "${YELLOW}[!] A remote named 'remote' already exists.${NC}"
|
||||
echo -e "${CYAN}Do you want to overwrite it? (y/n):${NC}"
|
||||
read -r overwrite_choice
|
||||
if [[ ! "$overwrite_choice" =~ ^[Yy]$ ]]; then
|
||||
echo -e "${YELLOW}[!] Configuration cancelled${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Create a temporary file without the [remote] section
|
||||
local temp_config=$(mktemp)
|
||||
local in_remote_section=false
|
||||
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
if [[ "$line" =~ ^\[remote\] ]]; then
|
||||
in_remote_section=true
|
||||
continue
|
||||
elif [[ "$line" =~ ^\[ ]] && [ "$in_remote_section" = true ]; then
|
||||
in_remote_section=false
|
||||
echo "$line" >> "$temp_config"
|
||||
elif [ "$in_remote_section" = false ]; then
|
||||
echo "$line" >> "$temp_config"
|
||||
fi
|
||||
done < "$rclone_config_file"
|
||||
|
||||
mv "$temp_config" "$rclone_config_file"
|
||||
fi
|
||||
|
||||
# Add new remote configuration
|
||||
cat >> "$rclone_config_file" << EOF
|
||||
|
||||
[remote]
|
||||
type = b2
|
||||
account = $B2_ACCOUNT_ID
|
||||
key = $B2_APPLICATION_KEY
|
||||
hard_delete = false
|
||||
EOF
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo -e "${GREEN}[✓] rclone configured successfully${NC}"
|
||||
else
|
||||
echo -e "${RED}[✗] Failed to configure rclone${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Test the configuration
|
||||
echo ""
|
||||
echo -e "${PURPLE}[*] Testing rclone configuration...${NC}"
|
||||
if rclone lsd remote: > /dev/null 2>&1; then
|
||||
echo -e "${GREEN}[✓] rclone connection test successful${NC}"
|
||||
|
||||
# List buckets if available
|
||||
echo ""
|
||||
echo -e "${CYAN}Available buckets:${NC}"
|
||||
rclone lsd remote: 2>/dev/null | awk '{print " - " $5}' || echo -e "${GRAY} (Unable to list buckets)${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}[!] Warning: Could not test connection to Backblaze B2${NC}"
|
||||
echo -e "${GRAY} Please verify your credentials are correct${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
show_separator
|
||||
echo -e "${WHITE}${BOLD}Configuration Complete!${NC}"
|
||||
show_separator
|
||||
echo ""
|
||||
echo -e "${GREEN}[✓] rclone has been configured with Backblaze B2${NC}"
|
||||
echo ""
|
||||
echo -e "${WHITE}${BOLD}Next Steps:${NC}"
|
||||
echo -e "${CYAN}1. Access CloudPanel at: ${BOLD}https://$(get_public_ip):$PANEL_PORT${NC}"
|
||||
echo -e "${CYAN}2. Go to Settings > Backups${NC}"
|
||||
echo -e "${CYAN}3. Select 'Custom Rclone Config' as the storage provider${NC}"
|
||||
if [ -n "$B2_BUCKET_NAME" ]; then
|
||||
echo -e "${CYAN}4. In 'Storage Directory', enter: ${BOLD}$B2_BUCKET_NAME${NC}"
|
||||
else
|
||||
echo -e "${CYAN}4. In 'Storage Directory', enter your Backblaze B2 bucket name${NC}"
|
||||
fi
|
||||
echo -e "${CYAN}5. Configure backup frequency and retention as needed${NC}"
|
||||
echo -e "${CYAN}6. Save the configuration${NC}"
|
||||
echo ""
|
||||
echo -e "${GRAY}[i] Note: The rclone remote is named 'remote' as required by CloudPanel${NC}"
|
||||
show_separator
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Main Menu
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
show_menu() {
|
||||
clear
|
||||
echo -e "${WHITE}${BOLD}CLOUDPANEL MANAGEMENT${NC}\n"
|
||||
echo -e " ${GREEN}[1]${NC} Install CloudPanel"
|
||||
echo -e " ${YELLOW}[2]${NC} Update CloudPanel"
|
||||
echo -e " ${CYAN}[3]${NC} View Status"
|
||||
echo -e " ${PURPLE}[4]${NC} Configure rclone for Backblaze B2"
|
||||
echo -e " ${RED}[0]${NC} Back to main menu"
|
||||
echo ""
|
||||
echo -n "Choice [0-4]: "
|
||||
}
|
||||
|
||||
main() {
|
||||
while true; do
|
||||
show_menu
|
||||
read -r choice
|
||||
echo ""
|
||||
|
||||
case $choice in
|
||||
1) install_cloudpanel ;;
|
||||
2) update_cloudpanel ;;
|
||||
3) show_status ;;
|
||||
4) configure_rclone_backblaze ;;
|
||||
0) return 75 ;;
|
||||
*) echo -e "${RED}Invalid option${NC}" ;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
read -p "Press Enter to continue..."
|
||||
done
|
||||
}
|
||||
|
||||
main
|
||||
|
||||
Executable
+265
@@ -0,0 +1,265 @@
|
||||
#!/bin/bash
|
||||
|
||||
# LXS - Coolify Installation Script
|
||||
# Description: Install and manage Coolify deployment platform
|
||||
# Author: LXS
|
||||
# Date: 2025
|
||||
|
||||
# Load LXS common library (colors, separator, run_spinner, loggers, helpers)
|
||||
LXS_RAW_BASE="${LXS_RAW_BASE:-https://git.hyko.cx/hykocx/lxs/raw/branch/main}"
|
||||
_lib=$(curl -fsSL "${LXS_RAW_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
|
||||
eval "$_lib"
|
||||
unset _lib
|
||||
export LXS_LOG_FILE="/tmp/lxs_coolify.log"
|
||||
|
||||
require_root "$0" "$@"
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Configuration
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
INSTALL_DIR="/data/coolify"
|
||||
PORT=8000
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Helper Functions
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
is_installed() {
|
||||
[ -d "$INSTALL_DIR" ] && docker ps | grep -q "coolify"
|
||||
}
|
||||
|
||||
check_requirements() {
|
||||
echo -e "${WHITE}Checking System Requirements...${NC}"
|
||||
|
||||
local cpu_cores=$(nproc)
|
||||
[ $cpu_cores -lt 2 ] && echo -e "${YELLOW}[!] Warning: Recommended minimum is 2 CPU cores (found: $cpu_cores)${NC}" || echo -e "${GREEN}[✓] CPU cores: $cpu_cores${NC}"
|
||||
|
||||
local total_mem_gb=$(($(grep MemTotal /proc/meminfo | awk '{print $2}') / 1024 / 1024))
|
||||
[ $total_mem_gb -lt 2 ] && echo -e "${YELLOW}[!] Warning: Recommended minimum is 2GB RAM (found: ${total_mem_gb}GB)${NC}" || echo -e "${GREEN}[✓] Memory: ${total_mem_gb}GB${NC}"
|
||||
|
||||
local available_gb=$(($(df / | awk 'NR==2 {print $4}') / 1024 / 1024))
|
||||
[ $available_gb -lt 30 ] && echo -e "${YELLOW}[!] Warning: Recommended minimum is 30GB free space (found: ${available_gb}GB)${NC}" || echo -e "${GREEN}[✓] Disk space: ${available_gb}GB available${NC}"
|
||||
|
||||
local arch=$(uname -m)
|
||||
if [[ "$arch" != "x86_64" && "$arch" != "aarch64" ]]; then
|
||||
echo -e "${RED}[✗] Unsupported architecture: $arch${NC}"
|
||||
return 1
|
||||
fi
|
||||
echo -e "${GREEN}[✓] Architecture: $arch${NC}"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Installation Functions
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
install_coolify() {
|
||||
local start_time=$(date +%s)
|
||||
|
||||
apt_noninteractive
|
||||
|
||||
echo -e "${WHITE}${BOLD}COOLIFY INSTALLATION${NC}\n"
|
||||
|
||||
if is_installed; then
|
||||
echo -e "${YELLOW}Coolify is already installed!${NC}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
require_disk_space 3072 || return 1
|
||||
check_requirements || return 1
|
||||
echo ""
|
||||
|
||||
wait_for_apt || return 1
|
||||
echo ""
|
||||
|
||||
echo -e "${WHITE}Running Official Coolify Installer${NC}"
|
||||
echo -e "${GRAY}[i] Installing Docker and all dependencies...${NC}"
|
||||
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${RED}[✗] Installation failed!${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo -e "${PURPLE}[*] Waiting for Coolify to start...${NC}"
|
||||
sleep 10
|
||||
|
||||
local max_retries=30
|
||||
local retry=0
|
||||
while [ $retry -lt $max_retries ]; do
|
||||
docker ps | grep -q "coolify" && break
|
||||
retry=$((retry + 1))
|
||||
sleep 2
|
||||
done
|
||||
|
||||
ufw_allow 80/tcp "Coolify HTTP"
|
||||
ufw_allow 443/tcp "Coolify HTTPS"
|
||||
ufw_allow "${PORT}/tcp" "Coolify dashboard"
|
||||
|
||||
local duration=$(( $(date +%s) - start_time ))
|
||||
local minutes=$((duration / 60))
|
||||
local seconds=$((duration % 60))
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}${BOLD}Installation Completed Successfully!${NC}"
|
||||
echo -e "${GRAY}Installation time: ${minutes}m ${seconds}s${NC}"
|
||||
echo ""
|
||||
echo -e "${CYAN}Access URL: ${BOLD}http://$(get_public_ip):$PORT${NC}"
|
||||
echo ""
|
||||
echo -e "${RED}${BOLD}IMPORTANT:${NC} ${RED}Create your admin account immediately!${NC}"
|
||||
echo -e "${RED}Visit the URL now to secure your installation.${NC}"
|
||||
}
|
||||
|
||||
update_coolify() {
|
||||
echo -e "${WHITE}${BOLD}COOLIFY UPDATE${NC}\n"
|
||||
|
||||
apt_noninteractive
|
||||
|
||||
if ! is_installed; then
|
||||
echo -e "${RED}Coolify is not installed!${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
|
||||
|
||||
[ $? -eq 0 ] && echo -e "${GREEN}[✓] Update completed${NC}" || echo -e "${RED}[✗] Update failed${NC}"
|
||||
}
|
||||
|
||||
show_status() {
|
||||
echo -e "${WHITE}${BOLD}COOLIFY STATUS${NC}\n"
|
||||
|
||||
if ! is_installed; then
|
||||
echo -e "${RED}Coolify is not installed!${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if docker ps | grep -q "coolify"; then
|
||||
echo -e "${GREEN}Coolify is running${NC}\n"
|
||||
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | grep -E "(NAMES|coolify)"
|
||||
echo ""
|
||||
echo -e "${CYAN}Access URL: ${BOLD}http://$(get_public_ip):$PORT${NC}"
|
||||
else
|
||||
echo -e "${RED}Coolify is not running${NC}"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Main Menu
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
show_post_installation_guide() {
|
||||
clear
|
||||
echo -e "${WHITE}${BOLD}COOLIFY POST-INSTALLATION GUIDE${NC}\n"
|
||||
|
||||
show_separator
|
||||
echo -e "${CYAN}${BOLD}1. Domain Name${NC}"
|
||||
echo -e "${GRAY} Settings -> General${NC}"
|
||||
echo -e "${WHITE} Name${NC}"
|
||||
echo ""
|
||||
|
||||
show_separator
|
||||
echo -e "${CYAN}${BOLD}2. Timezone${NC}"
|
||||
echo -e "${GRAY} Settings -> General${NC}"
|
||||
echo -e "${WHITE} Timezone: Toronto${NC}"
|
||||
echo ""
|
||||
|
||||
show_separator
|
||||
echo -e "${CYAN}${BOLD}3. Do Not Track Activation${NC}"
|
||||
echo -e "${GRAY} Settings -> Advanced${NC}"
|
||||
echo ""
|
||||
|
||||
show_separator
|
||||
echo -e "${CYAN}${BOLD}4. Discord Notifications${NC}"
|
||||
echo -e "${GRAY} Notifications -> Discord${NC}"
|
||||
echo ""
|
||||
echo -e "${WHITE} Configuration:${NC}"
|
||||
echo -e " ${GREEN}[x]${NC} Deployment Success"
|
||||
echo -e " ${GREEN}[x]${NC} Deployment Failure"
|
||||
echo -e " ${GRAY}[ ]${NC} Container Status Changes"
|
||||
echo -e " ${GREEN}[x]${NC} Backup Success"
|
||||
echo -e " ${GREEN}[x]${NC} Backup Failure"
|
||||
echo -e " ${GRAY}[ ]${NC} Scheduled Task Success"
|
||||
echo -e " ${GREEN}[x]${NC} Scheduled Task Failure"
|
||||
echo -e " ${GRAY}[ ]${NC} Docker Cleanup Success"
|
||||
echo -e " ${GREEN}[x]${NC} Docker Cleanup Failure"
|
||||
echo -e " ${GREEN}[x]${NC} Server Disk Usage"
|
||||
echo -e " ${GREEN}[x]${NC} Server Reachable"
|
||||
echo -e " ${GREEN}[x]${NC} Server Unreachable"
|
||||
echo ""
|
||||
|
||||
show_separator
|
||||
echo -e "${CYAN}${BOLD}5. S3 Storage for Backups${NC}"
|
||||
echo -e "${GRAY} S3 Storages${NC}"
|
||||
echo ""
|
||||
echo -e "${WHITE} Configuration:${NC}"
|
||||
echo -e " ${GRAY}Name:${NC} Provider name (ex. Backblaze)"
|
||||
echo -e " ${GRAY}Endpoint:${NC} "
|
||||
echo -e " ${GRAY}Bucket:${NC} "
|
||||
echo -e " ${GRAY}Access Key:${NC} "
|
||||
echo -e " ${GRAY}Secret Key:${NC} "
|
||||
echo ""
|
||||
|
||||
show_separator
|
||||
echo -e "${CYAN}${BOLD}6. Coolify Backup Configuration${NC}"
|
||||
echo -e "${GRAY} Settings -> Backup${NC}"
|
||||
echo ""
|
||||
echo -e "${WHITE} Configuration:${NC}"
|
||||
echo -e " ${GREEN}[x]${NC} S3 Enabled"
|
||||
echo -e " ${GRAY}Number of backups to keep:${NC} 7"
|
||||
echo ""
|
||||
|
||||
show_separator
|
||||
echo -e "${CYAN}${BOLD}7. Server Timezone${NC}"
|
||||
echo -e "${GRAY} Servers -> localhost -> General${NC}"
|
||||
echo -e "${WHITE} Timezone: Toronto${NC}"
|
||||
echo ""
|
||||
|
||||
show_separator
|
||||
echo -e "${CYAN}${BOLD}8. Docker Cleanup Schedule${NC}"
|
||||
echo -e "${GRAY} Servers -> localhost -> Docker Cleanup${NC}"
|
||||
echo ""
|
||||
echo -e "${WHITE} Schedule (every hour):${NC}"
|
||||
echo -e " ${GRAY}0 * * * *${NC}"
|
||||
echo ""
|
||||
|
||||
show_separator
|
||||
echo ""
|
||||
}
|
||||
|
||||
show_menu() {
|
||||
clear
|
||||
echo -e "${WHITE}${BOLD}COOLIFY MANAGEMENT${NC}\n"
|
||||
echo -e " ${GREEN}[1]${NC} Install Coolify"
|
||||
echo -e " ${YELLOW}[2]${NC} Update Coolify"
|
||||
echo -e " ${CYAN}[3]${NC} View Status"
|
||||
echo -e " ${PURPLE}[4]${NC} Post-Installation Guide"
|
||||
echo -e " ${RED}[0]${NC} Back to main menu"
|
||||
echo ""
|
||||
echo -n "Choice [0-4]: "
|
||||
}
|
||||
|
||||
main() {
|
||||
while true; do
|
||||
show_menu
|
||||
read -r choice
|
||||
echo ""
|
||||
|
||||
case $choice in
|
||||
1) install_coolify ;;
|
||||
2) update_coolify ;;
|
||||
3) show_status ;;
|
||||
4) show_post_installation_guide ;;
|
||||
0) return 75 ;;
|
||||
*) echo -e "${RED}Invalid option${NC}" ;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
read -p "Press Enter to continue..."
|
||||
done
|
||||
}
|
||||
|
||||
main
|
||||
Executable
+89
@@ -0,0 +1,89 @@
|
||||
#!/bin/bash
|
||||
|
||||
# LXS - Apps index
|
||||
# Description: Interactive menu listing the application installers in apps/
|
||||
# Author: LXS
|
||||
# Date: 2025
|
||||
|
||||
# Load LXS common library (colors, separator, run_spinner, loggers)
|
||||
LXS_RAW_BASE="${LXS_RAW_BASE:-https://git.hyko.cx/hykocx/lxs/raw/branch/main}"
|
||||
_lib=$(curl -fsSL "${LXS_RAW_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
|
||||
eval "$_lib"
|
||||
unset _lib
|
||||
|
||||
# Run a sibling app script. Prefers a file next to this script (installed
|
||||
# layout); falls back to downloading from LXS_RAW_BASE.
|
||||
run_sibling() {
|
||||
local script_path=$1
|
||||
shift
|
||||
local script_name self_dir resolved src="${BASH_SOURCE[0]}"
|
||||
script_name=$(basename "$script_path")
|
||||
|
||||
if [ -n "$src" ]; then
|
||||
resolved=$(readlink -f "$src" 2>/dev/null) \
|
||||
|| resolved=$(realpath "$src" 2>/dev/null) \
|
||||
|| resolved="$src"
|
||||
self_dir=$(dirname "$resolved")
|
||||
fi
|
||||
|
||||
if [ -n "$self_dir" ] && [ -f "${self_dir}/${script_name}" ]; then
|
||||
chmod +x "${self_dir}/${script_name}" 2>/dev/null || true
|
||||
"${self_dir}/${script_name}" "$@"
|
||||
return $?
|
||||
fi
|
||||
|
||||
local temp_file exit_code
|
||||
temp_file=$(mktemp "/tmp/lxs.${script_name%.*}.XXXXXX.sh")
|
||||
echo -e "${CYAN}[..] Fetching ${BOLD}${script_name}${NC}${CYAN}...${NC}"
|
||||
if curl -fsSL -H "Cache-Control: no-cache" -o "${temp_file}" "${LXS_RAW_BASE}/${script_path}"; then
|
||||
echo -e "${GREEN}[OK] Payload acquired${NC}"
|
||||
chmod +x "${temp_file}"
|
||||
"${temp_file}" "$@"
|
||||
exit_code=$?
|
||||
rm -f "${temp_file}"
|
||||
return $exit_code
|
||||
else
|
||||
echo -e "${RED}[KO] Failed to download ${script_path}${NC}"
|
||||
rm -f "${temp_file}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
menu_apps() {
|
||||
while true; do
|
||||
clear
|
||||
show_box_top "APPLICATIONS" "APP_REPOSITORY"
|
||||
echo ""
|
||||
show_menu_item "01" "Coolify"
|
||||
show_menu_item "02" "Pterodactyl Panel"
|
||||
show_menu_item "03" "Uptime Kuma"
|
||||
show_menu_item "04" "CloudPanel"
|
||||
show_menu_item "05" "Proxmox VE Tools"
|
||||
show_menu_item "00" "BACK" "" exit
|
||||
echo ""
|
||||
show_box_bottom
|
||||
echo ""
|
||||
show_prompt
|
||||
read -r choice
|
||||
|
||||
child_rc=0
|
||||
case $choice in
|
||||
1|01) run_sibling "apps/coolify.sh"; child_rc=$? ;;
|
||||
2|02) run_sibling "apps/pterodactyl.sh"; child_rc=$? ;;
|
||||
3|03) run_sibling "apps/uptime-kuma.sh"; child_rc=$? ;;
|
||||
4|04) run_sibling "apps/cloudpanel.sh"; child_rc=$? ;;
|
||||
5|05) run_sibling "apps/proxmox.sh"; child_rc=$? ;;
|
||||
0|00) return ;;
|
||||
*) echo -e "${RED}[KO] Invalid protocol. Select 0-5.${NC}"; sleep 1; continue ;;
|
||||
esac
|
||||
|
||||
# Exit code 75 from a child means it already paused on its own
|
||||
# (its own "Back" or end-of-run prompt) — skip the redundant prompt.
|
||||
if [ "$child_rc" -ne 75 ]; then
|
||||
echo ""
|
||||
read -r -p "Press Enter to continue..."
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
menu_apps
|
||||
Executable
+386
@@ -0,0 +1,386 @@
|
||||
#!/bin/bash
|
||||
|
||||
# LXS - Proxmox VE Management Script
|
||||
# Description: Tools for managing and troubleshooting Proxmox VE
|
||||
# Author: LXS
|
||||
# Date: 2025
|
||||
|
||||
# Load LXS common library (colors, separator, run_spinner, loggers, helpers)
|
||||
LXS_RAW_BASE="${LXS_RAW_BASE:-https://git.hyko.cx/hykocx/lxs/raw/branch/main}"
|
||||
_lib=$(curl -fsSL "${LXS_RAW_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
|
||||
eval "$_lib"
|
||||
unset _lib
|
||||
export LXS_LOG_FILE="/tmp/lxs_proxmox.log"
|
||||
|
||||
require_root "$0" "$@"
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Configuration
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
LOCKFILE="/var/lib/pve-cluster/.pmxcfs.lockfile"
|
||||
SERVICE_NAME="pve-cluster"
|
||||
PANEL_PORT=8006
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Helper Functions
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
is_proxmox() {
|
||||
[ -f /etc/pve/.version ] || command -v pveversion &> /dev/null
|
||||
}
|
||||
|
||||
get_service_status() {
|
||||
if systemctl is-active --quiet $SERVICE_NAME; then
|
||||
echo -e "${GREEN}running${NC}"
|
||||
else
|
||||
echo -e "${RED}stopped${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
check_proxmox() {
|
||||
if ! is_proxmox; then
|
||||
echo -e "${RED}[!] This is not a Proxmox VE system!${NC}"
|
||||
echo -e "${YELLOW} This script is designed for Proxmox VE only.${NC}"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Login Fix Functions
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
fix_login_issue() {
|
||||
local start_time=$(date +%s)
|
||||
|
||||
echo -e "${WHITE}${BOLD}PROXMOX LOGIN FIX${NC}\n"
|
||||
|
||||
check_proxmox || return 1
|
||||
|
||||
echo -e "${CYAN}This will reset the pve-cluster lockfile to fix login issues.${NC}"
|
||||
echo ""
|
||||
|
||||
echo "[1/3] Stopping pve-cluster service..."
|
||||
run_spinner "Stopping $SERVICE_NAME..." "systemctl stop $SERVICE_NAME"
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${RED}[!] Failed to stop pve-cluster service${NC}"
|
||||
return 1
|
||||
fi
|
||||
echo ""
|
||||
|
||||
echo "[2/3] Removing lockfile..."
|
||||
if [ -f "$LOCKFILE" ]; then
|
||||
run_spinner "Removing lockfile..." "rm -f '$LOCKFILE'"
|
||||
echo -e "${GRAY} Lockfile was present and has been removed${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}[!] Lockfile not found (may already be removed)${NC}"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
echo "[3/3] Starting pve-cluster service..."
|
||||
run_spinner "Starting $SERVICE_NAME..." "systemctl start $SERVICE_NAME"
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${RED}[!] Failed to start pve-cluster service${NC}"
|
||||
echo -e "${YELLOW} Check logs with: journalctl -xe${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
sleep 2
|
||||
|
||||
local duration=$(( $(date +%s) - start_time ))
|
||||
local server_ip=$(hostname -I | awk '{print $1}')
|
||||
|
||||
echo ""
|
||||
show_separator
|
||||
echo -e "${GREEN}${BOLD}Fix Completed!${NC}"
|
||||
echo -e "${GRAY}Time: ${duration}s${NC}"
|
||||
echo ""
|
||||
echo -e "${WHITE}Service Status:${NC} $(get_service_status)"
|
||||
echo ""
|
||||
echo -e "${CYAN}You should now be able to login to the Proxmox web interface.${NC}"
|
||||
echo -e "${WHITE}Panel URL: ${GREEN}${BOLD}https://$server_ip:$PANEL_PORT${NC}"
|
||||
show_separator
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Service Management Functions
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
restart_pve_cluster() {
|
||||
echo -e "${WHITE}${BOLD}RESTART PVE-CLUSTER${NC}\n"
|
||||
|
||||
check_proxmox || return 1
|
||||
|
||||
run_spinner "Restarting $SERVICE_NAME..." "systemctl restart $SERVICE_NAME"
|
||||
|
||||
sleep 2
|
||||
|
||||
echo ""
|
||||
echo -e "${WHITE}Service Status:${NC} $(get_service_status)"
|
||||
}
|
||||
|
||||
restart_all_services() {
|
||||
echo -e "${WHITE}${BOLD}RESTART ALL PROXMOX SERVICES${NC}\n"
|
||||
|
||||
check_proxmox || return 1
|
||||
|
||||
local services=("pve-cluster" "pvedaemon" "pveproxy" "pvestatd")
|
||||
|
||||
for service in "${services[@]}"; do
|
||||
if systemctl list-unit-files | grep -q "^$service.service"; then
|
||||
run_spinner "Restarting $service..." "systemctl restart $service"
|
||||
fi
|
||||
done
|
||||
|
||||
sleep 2
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}[✓] All services restarted${NC}"
|
||||
echo ""
|
||||
echo -e "${WHITE}Panel URL: ${GREEN}${BOLD}https://$(hostname -I | awk '{print $1}'):$PANEL_PORT${NC}"
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Status Functions
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
show_status() {
|
||||
echo -e "${WHITE}${BOLD}PROXMOX VE STATUS${NC}\n"
|
||||
|
||||
check_proxmox || return 1
|
||||
|
||||
# Proxmox version
|
||||
echo -e "${WHITE}Proxmox Version:${NC}"
|
||||
pveversion 2>/dev/null || echo -e "${GRAY}Unable to determine version${NC}"
|
||||
echo ""
|
||||
|
||||
# Service statuses
|
||||
echo -e "${WHITE}Service Status:${NC}"
|
||||
local services=("pve-cluster" "pvedaemon" "pveproxy" "pvestatd")
|
||||
|
||||
for service in "${services[@]}"; do
|
||||
if systemctl list-unit-files | grep -q "^$service.service"; then
|
||||
if systemctl is-active --quiet $service; then
|
||||
echo -e " ${GREEN}[✓]${NC} $service"
|
||||
else
|
||||
echo -e " ${RED}[✗]${NC} $service"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
|
||||
# Lockfile status
|
||||
if [ -f "$LOCKFILE" ]; then
|
||||
echo -e "${WHITE}Lockfile:${NC} ${YELLOW}Present${NC} ($LOCKFILE)"
|
||||
else
|
||||
echo -e "${WHITE}Lockfile:${NC} ${GREEN}Not present${NC}"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Access URL
|
||||
local server_ip=$(hostname -I | awk '{print $1}')
|
||||
echo -e "${WHITE}Panel URL: ${GREEN}${BOLD}https://$server_ip:$PANEL_PORT${NC}"
|
||||
echo ""
|
||||
|
||||
# Cluster info
|
||||
if command -v pvecm &> /dev/null; then
|
||||
echo -e "${WHITE}Cluster Status:${NC}"
|
||||
pvecm status 2>/dev/null | head -5 || echo -e "${GRAY}Not in a cluster or unable to get status${NC}"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
show_storage_status() {
|
||||
echo -e "${WHITE}${BOLD}PROXMOX STORAGE STATUS${NC}\n"
|
||||
|
||||
check_proxmox || return 1
|
||||
|
||||
# Storage list
|
||||
echo -e "${WHITE}Storage Configuration:${NC}"
|
||||
if command -v pvesm &> /dev/null; then
|
||||
pvesm status 2>/dev/null || echo -e "${GRAY}Unable to get storage status${NC}"
|
||||
else
|
||||
echo -e "${GRAY}pvesm command not available${NC}"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Disk usage
|
||||
echo -e "${WHITE}Disk Usage:${NC}"
|
||||
df -h / /var /tmp 2>/dev/null | head -5
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Maintenance Functions
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
clear_cache() {
|
||||
echo -e "${WHITE}${BOLD}CLEAR PROXMOX CACHE${NC}\n"
|
||||
|
||||
check_proxmox || return 1
|
||||
|
||||
echo -e "${CYAN}This will clear various Proxmox caches and temporary files.${NC}"
|
||||
echo ""
|
||||
|
||||
run_spinner "Clearing apt cache..." "apt clean"
|
||||
run_spinner "Clearing journal logs (older than 7 days)..." "journalctl --vacuum-time=7d"
|
||||
|
||||
# Clear task logs older than 30 days
|
||||
if [ -d "/var/log/pve/tasks" ]; then
|
||||
run_spinner "Clearing old task logs..." "find /var/log/pve/tasks -type f -mtime +30 -delete"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}[✓] Cache cleared${NC}"
|
||||
}
|
||||
|
||||
update_proxmox() {
|
||||
echo -e "${WHITE}${BOLD}UPDATE PROXMOX VE${NC}\n"
|
||||
|
||||
check_proxmox || return 1
|
||||
|
||||
require_disk_space 1024 || return 1
|
||||
|
||||
# Configure non-interactive mode
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
export NEEDRESTART_MODE=a
|
||||
export NEEDRESTART_SUSPEND=1
|
||||
|
||||
local start_time=$(date +%s)
|
||||
|
||||
run_spinner "Updating package lists..." "apt update"
|
||||
|
||||
echo ""
|
||||
echo -e "${PURPLE}[*] Upgrading packages...${NC}"
|
||||
show_separator
|
||||
apt dist-upgrade -y
|
||||
local exit_code=$?
|
||||
show_separator
|
||||
|
||||
if [ $exit_code -eq 0 ]; then
|
||||
local duration=$(( $(date +%s) - start_time ))
|
||||
echo ""
|
||||
echo -e "${GREEN}${BOLD}Update Completed!${NC}"
|
||||
echo -e "${GRAY}Time: $((duration / 60))m $((duration % 60))s${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}[!] A reboot may be required if the kernel was updated.${NC}"
|
||||
else
|
||||
echo ""
|
||||
echo -e "${RED}[✗] Update failed${NC}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Network Functions
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
show_network_info() {
|
||||
echo -e "${WHITE}${BOLD}PROXMOX NETWORK INFO${NC}\n"
|
||||
|
||||
check_proxmox || return 1
|
||||
|
||||
local server_ip=$(hostname -I | awk '{print $1}')
|
||||
local public_ip=$(get_public_ip)
|
||||
|
||||
echo -e "${WHITE}Hostname:${NC} $(hostname)"
|
||||
echo -e "${WHITE}Local IP:${NC} $server_ip"
|
||||
echo -e "${WHITE}Public IP:${NC} $public_ip"
|
||||
echo ""
|
||||
|
||||
echo -e "${WHITE}Network Interfaces:${NC}"
|
||||
ip -br addr show 2>/dev/null || ifconfig 2>/dev/null | grep -E "^[a-z]|inet "
|
||||
echo ""
|
||||
|
||||
echo -e "${WHITE}Bridges:${NC}"
|
||||
if command -v brctl &> /dev/null; then
|
||||
brctl show 2>/dev/null || echo -e "${GRAY}No bridges found${NC}"
|
||||
else
|
||||
ip link show type bridge 2>/dev/null || echo -e "${GRAY}No bridges found${NC}"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Firewall
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
open_firewall_ports() {
|
||||
echo -e "${WHITE}${BOLD}OPEN PROXMOX FIREWALL PORTS${NC}\n"
|
||||
|
||||
check_proxmox || return 1
|
||||
|
||||
if ! command -v ufw >/dev/null 2>&1; then
|
||||
echo -e "${YELLOW}[!] UFW is not installed on this host. Nothing to do.${NC}"
|
||||
echo -e "${GRAY} Proxmox uses its own pve-firewall; UFW is optional.${NC}"
|
||||
return 0
|
||||
fi
|
||||
if ! ufw status 2>/dev/null | grep -q "Status: active"; then
|
||||
echo -e "${YELLOW}[!] UFW is installed but inactive. Enable it first.${NC}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
ufw_allow "${PANEL_PORT}/tcp" "Proxmox web UI"
|
||||
ufw_allow 5900:5999/tcp "Proxmox VNC console"
|
||||
ufw_allow 3128/tcp "Proxmox SPICE proxy"
|
||||
|
||||
echo ""
|
||||
echo -e "${GRAY}[i] For clustered nodes, also open: 5404-5405/udp (corosync), 60000-60050/tcp (live migration).${NC}"
|
||||
echo -e "${GRAY}[i] If using NFS storage: 111/tcp+udp and 2049/tcp.${NC}"
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Main Menu
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
show_menu() {
|
||||
clear
|
||||
echo -e "${WHITE}${BOLD}PROXMOX VE MANAGEMENT${NC}\n"
|
||||
echo -e " ${GREEN}[1]${NC} Fix Login Issue (reset lockfile)"
|
||||
echo -e " ${YELLOW}[2]${NC} Restart pve-cluster service"
|
||||
echo -e " ${YELLOW}[3]${NC} Restart all Proxmox services"
|
||||
echo -e " ${CYAN}[4]${NC} View System Status"
|
||||
echo -e " ${CYAN}[5]${NC} View Storage Status"
|
||||
echo -e " ${CYAN}[6]${NC} View Network Info"
|
||||
echo -e " ${PURPLE}[7]${NC} Update Proxmox VE"
|
||||
echo -e " ${PURPLE}[8]${NC} Clear Cache"
|
||||
echo -e " ${PURPLE}[9]${NC} Open Firewall Ports (UFW)"
|
||||
echo -e " ${RED}[0]${NC} Back to main menu"
|
||||
echo ""
|
||||
echo -n "Choice [0-9]: "
|
||||
}
|
||||
|
||||
main() {
|
||||
while true; do
|
||||
show_menu
|
||||
read -r choice
|
||||
echo ""
|
||||
|
||||
case $choice in
|
||||
1) fix_login_issue ;;
|
||||
2) restart_pve_cluster ;;
|
||||
3) restart_all_services ;;
|
||||
4) show_status ;;
|
||||
5) show_storage_status ;;
|
||||
6) show_network_info ;;
|
||||
7) update_proxmox ;;
|
||||
8) clear_cache ;;
|
||||
9) open_firewall_ports ;;
|
||||
0) return 75 ;;
|
||||
*) echo -e "${RED}Invalid option${NC}" ;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
read -p "Press Enter to continue..."
|
||||
done
|
||||
}
|
||||
|
||||
main
|
||||
|
||||
Executable
+337
@@ -0,0 +1,337 @@
|
||||
#!/bin/bash
|
||||
|
||||
# LXS - Pterodactyl Installation Script
|
||||
# Description: Install Pterodactyl Panel + Wings
|
||||
# Author: LXS
|
||||
# Date: 2025
|
||||
|
||||
# Load LXS common library (colors, separator, run_spinner, loggers, helpers)
|
||||
LXS_RAW_BASE="${LXS_RAW_BASE:-https://git.hyko.cx/hykocx/lxs/raw/branch/main}"
|
||||
_lib=$(curl -fsSL "${LXS_RAW_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
|
||||
eval "$_lib"
|
||||
unset _lib
|
||||
export LXS_LOG_FILE="/tmp/lxs_pterodactyl.log"
|
||||
|
||||
require_root "$0" "$@"
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Configuration
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
PANEL_DIR="/var/www/pterodactyl"
|
||||
WINGS_DIR="/etc/pterodactyl"
|
||||
DB_NAME="panel"
|
||||
DB_USER="pterodactyl"
|
||||
DB_HOST="127.0.0.1"
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Helper Functions
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
is_installed() {
|
||||
[ -d "$PANEL_DIR" ] && systemctl list-unit-files | grep -q "pteroq.service"
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Installation Functions
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
install_dependencies() {
|
||||
apt_noninteractive
|
||||
|
||||
run_spinner "Updating system..." "apt update -qq"
|
||||
run_spinner "Installing dependencies..." "apt install -y -qq software-properties-common curl -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold'"
|
||||
run_spinner "Adding PHP repository..." "LC_ALL=C.UTF-8 add-apt-repository -y ppa:ondrej/php && apt update -qq"
|
||||
run_spinner "Installing PHP and services..." "apt install -y -qq php8.3 php8.3-{cli,gd,mysql,mbstring,bcmath,xml,fpm,curl,zip} mariadb-server nginx tar unzip git redis-server -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold'"
|
||||
}
|
||||
|
||||
install_composer() {
|
||||
run_spinner "Installing Composer..." "curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer --quiet"
|
||||
}
|
||||
|
||||
setup_database() {
|
||||
DB_PASS=$(generate_password)
|
||||
|
||||
echo -e "${PURPLE}[*] Configuring database...${NC}"
|
||||
systemctl start mariadb && systemctl enable mariadb
|
||||
|
||||
mysql -u root <<MYSQL_SCRIPT
|
||||
CREATE DATABASE IF NOT EXISTS ${DB_NAME};
|
||||
CREATE USER IF NOT EXISTS '${DB_USER}'@'${DB_HOST}' IDENTIFIED BY '${DB_PASS}';
|
||||
GRANT ALL PRIVILEGES ON ${DB_NAME}.* TO '${DB_USER}'@'${DB_HOST}';
|
||||
FLUSH PRIVILEGES;
|
||||
MYSQL_SCRIPT
|
||||
|
||||
[ $? -eq 0 ] && echo -e "${GREEN}[✓] Database configured${NC}" || { echo -e "${RED}[✗] Database failed${NC}"; return 1; }
|
||||
}
|
||||
|
||||
install_panel() {
|
||||
mkdir -p $PANEL_DIR && cd $PANEL_DIR
|
||||
run_spinner "Downloading Panel..." "curl -Lo panel.tar.gz https://github.com/pterodactyl/panel/releases/latest/download/panel.tar.gz"
|
||||
run_spinner "Extracting..." "tar -xzf panel.tar.gz"
|
||||
run_spinner "Setting permissions..." "chmod -R 755 storage/* bootstrap/cache/"
|
||||
}
|
||||
|
||||
configure_panel() {
|
||||
cd $PANEL_DIR
|
||||
|
||||
echo -e "${PURPLE}[*] Installing dependencies (this may take a few minutes)...${NC}"
|
||||
export COMPOSER_ALLOW_SUPERUSER=1
|
||||
COMPOSER_ALLOW_SUPERUSER=1 composer install --no-dev --optimize-autoloader --no-interaction > /tmp/lxs_composer.log 2>&1
|
||||
|
||||
cp .env.example .env
|
||||
php artisan key:generate --force
|
||||
|
||||
sed -i "s/DB_DATABASE=.*/DB_DATABASE=${DB_NAME}/" .env
|
||||
sed -i "s/DB_USERNAME=.*/DB_USERNAME=${DB_USER}/" .env
|
||||
sed -i "s/DB_PASSWORD=.*/DB_PASSWORD=${DB_PASS}/" .env
|
||||
|
||||
run_spinner "Running migrations..." "php artisan migrate --seed --force"
|
||||
|
||||
echo ""
|
||||
echo -e "${WHITE}${BOLD}Admin Account Setup${NC}"
|
||||
read -p "Email: " ADMIN_EMAIL
|
||||
read -p "Username: " ADMIN_USERNAME
|
||||
read -p "First Name: " ADMIN_FIRSTNAME
|
||||
read -p "Last Name: " ADMIN_LASTNAME
|
||||
read -sp "Password: " ADMIN_PASSWORD
|
||||
echo ""
|
||||
|
||||
php artisan p:user:make --email="$ADMIN_EMAIL" --username="$ADMIN_USERNAME" --name-first="$ADMIN_FIRSTNAME" --name-last="$ADMIN_LASTNAME" --password="$ADMIN_PASSWORD" --admin=1 --no-interaction
|
||||
|
||||
chown -R www-data:www-data $PANEL_DIR
|
||||
}
|
||||
|
||||
configure_nginx() {
|
||||
local domain=$1
|
||||
|
||||
cat > /etc/nginx/sites-available/pterodactyl.conf <<'EOF'
|
||||
server {
|
||||
listen 80;
|
||||
server_name DOMAIN_PLACEHOLDER;
|
||||
root /var/www/pterodactyl/public;
|
||||
index index.php;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
|
||||
fastcgi_index index.php;
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
sed -i "s/DOMAIN_PLACEHOLDER/$domain/g" /etc/nginx/sites-available/pterodactyl.conf
|
||||
ln -sf /etc/nginx/sites-available/pterodactyl.conf /etc/nginx/sites-enabled/
|
||||
rm -f /etc/nginx/sites-enabled/default
|
||||
|
||||
nginx -t && systemctl restart nginx php8.3-fpm
|
||||
}
|
||||
|
||||
configure_ssl() {
|
||||
local domain=$1
|
||||
local email=$2
|
||||
|
||||
run_spinner "Installing Certbot..." "apt install -y -qq certbot python3-certbot-nginx -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold'"
|
||||
certbot --nginx -d $domain --non-interactive --agree-tos --email $email --redirect 2>/dev/null
|
||||
|
||||
[ $? -eq 0 ] && cd $PANEL_DIR && sed -i "s|APP_URL=.*|APP_URL=https://${domain}|" .env
|
||||
}
|
||||
|
||||
setup_services() {
|
||||
cat > /etc/systemd/system/pteroq.service <<'EOF'
|
||||
[Unit]
|
||||
Description=Pterodactyl Queue Worker
|
||||
After=redis-server.service
|
||||
|
||||
[Service]
|
||||
User=www-data
|
||||
Group=www-data
|
||||
Restart=always
|
||||
ExecStart=/usr/bin/php /var/www/pterodactyl/artisan queue:work --queue=high,standard,low --sleep=3 --tries=3
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl enable --now redis-server pteroq.service
|
||||
(crontab -l 2>/dev/null; echo "* * * * * php /var/www/pterodactyl/artisan schedule:run >> /dev/null 2>&1") | crontab -
|
||||
}
|
||||
|
||||
install_docker() {
|
||||
run_spinner "Installing Docker..." "curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg && \
|
||||
echo \"deb [arch=\$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \$(lsb_release -cs) stable\" | tee /etc/apt/sources.list.d/docker.list > /dev/null && \
|
||||
apt update -qq && apt install -y -qq docker-ce docker-ce-cli containerd.io -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold'"
|
||||
systemctl enable --now docker
|
||||
}
|
||||
|
||||
install_wings() {
|
||||
mkdir -p $WINGS_DIR
|
||||
run_spinner "Installing Wings..." "curl -L -o /usr/local/bin/wings https://github.com/pterodactyl/wings/releases/latest/download/wings_linux_amd64 && chmod +x /usr/local/bin/wings"
|
||||
|
||||
cat > /etc/systemd/system/wings.service <<'EOF'
|
||||
[Unit]
|
||||
Description=Pterodactyl Wings
|
||||
After=docker.service
|
||||
Requires=docker.service
|
||||
|
||||
[Service]
|
||||
User=root
|
||||
WorkingDirectory=/etc/pterodactyl
|
||||
ExecStart=/usr/local/bin/wings
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Main Installation
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
install_pterodactyl() {
|
||||
local start_time=$(date +%s)
|
||||
|
||||
# Configure non-interactive mode globally
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
export NEEDRESTART_MODE=a
|
||||
export NEEDRESTART_SUSPEND=1
|
||||
|
||||
echo -e "${WHITE}${BOLD}PTERODACTYL INSTALLATION${NC}\n"
|
||||
|
||||
if is_installed; then
|
||||
echo -e "${YELLOW}Pterodactyl is already installed!${NC}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
require_disk_space 3072 || return 1
|
||||
|
||||
echo "[1/10] Installing dependencies..."
|
||||
install_dependencies && install_composer
|
||||
echo ""
|
||||
|
||||
echo "[2/10] Setting up database..."
|
||||
setup_database
|
||||
echo ""
|
||||
|
||||
echo "[3/10] Installing Panel..."
|
||||
install_panel
|
||||
echo ""
|
||||
|
||||
echo "[4/10] Configuring Panel..."
|
||||
configure_panel
|
||||
echo ""
|
||||
|
||||
echo "[5/10] Domain configuration..."
|
||||
read -p "Panel domain (e.g., panel.example.com): " PANEL_DOMAIN
|
||||
read -p "Wings domain (e.g., wings.example.com): " WINGS_DOMAIN
|
||||
read -p "Email for SSL: " SSL_EMAIL
|
||||
echo ""
|
||||
|
||||
echo "[6/10] Configuring Nginx..."
|
||||
configure_nginx "$PANEL_DOMAIN"
|
||||
echo ""
|
||||
|
||||
echo "[7/10] Configuring SSL..."
|
||||
configure_ssl "$PANEL_DOMAIN" "$SSL_EMAIL"
|
||||
echo ""
|
||||
|
||||
echo "[8/10] Setting up services..."
|
||||
setup_services
|
||||
echo ""
|
||||
|
||||
echo "[9/10] Installing Wings..."
|
||||
install_docker && install_wings
|
||||
echo ""
|
||||
|
||||
echo "[10/10] Wings configuration..."
|
||||
echo -e "${YELLOW}Configure Wings from Panel: Admin -> Nodes -> Create Node${NC}"
|
||||
echo -e "${GRAY}Node settings:${NC}"
|
||||
echo -e " FQDN: ${CYAN}$WINGS_DOMAIN${NC}"
|
||||
echo -e " SSL: ${CYAN}Yes${NC}"
|
||||
echo -e " Port: ${CYAN}8080${NC}"
|
||||
echo ""
|
||||
|
||||
read -p "Press Enter when ready to paste Wings config..."
|
||||
echo -e "${CYAN}Paste config and press Ctrl+D:${NC}"
|
||||
cat > /etc/pterodactyl/config.yml
|
||||
|
||||
[ -s /etc/pterodactyl/config.yml ] && systemctl enable --now wings
|
||||
|
||||
ufw_allow 80/tcp "Pterodactyl HTTP"
|
||||
ufw_allow 443/tcp "Pterodactyl HTTPS"
|
||||
ufw_allow 8080/tcp "Pterodactyl Wings daemon"
|
||||
ufw_allow 2022/tcp "Pterodactyl Wings SFTP"
|
||||
|
||||
local duration=$(( $(date +%s) - start_time ))
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}${BOLD}Installation Completed!${NC}"
|
||||
echo -e "${GRAY}Time: $((duration / 60))m $((duration % 60))s${NC}"
|
||||
echo ""
|
||||
echo -e "${CYAN}Panel: ${BOLD}https://$PANEL_DOMAIN${NC}"
|
||||
echo -e "${CYAN}Wings: ${BOLD}$WINGS_DOMAIN:8080${NC}"
|
||||
echo ""
|
||||
echo -e "${WHITE}Database Credentials:${NC}"
|
||||
echo -e " Host: $DB_HOST"
|
||||
echo -e " Database: $DB_NAME"
|
||||
echo -e " User: $DB_USER"
|
||||
echo -e " Password: $DB_PASS"
|
||||
}
|
||||
|
||||
show_status() {
|
||||
echo -e "${WHITE}${BOLD}PTERODACTYL STATUS${NC}\n"
|
||||
|
||||
if ! is_installed; then
|
||||
echo -e "${RED}Pterodactyl is not installed!${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
systemctl is-active --quiet pteroq && echo -e "${GREEN}[✓] Panel Queue${NC}" || echo -e "${RED}[✗] Panel Queue${NC}"
|
||||
systemctl is-active --quiet nginx && echo -e "${GREEN}[✓] Web Server${NC}" || echo -e "${RED}[✗] Web Server${NC}"
|
||||
systemctl is-active --quiet wings && echo -e "${GREEN}[✓] Wings${NC}" || echo -e "${YELLOW}[!] Wings (needs config)${NC}"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Main Menu
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
show_menu() {
|
||||
clear
|
||||
echo -e "${WHITE}${BOLD}PTERODACTYL MANAGEMENT${NC}\n"
|
||||
echo -e " ${GREEN}[1]${NC} Install Pterodactyl"
|
||||
echo -e " ${CYAN}[2]${NC} View Status"
|
||||
echo -e " ${RED}[0]${NC} Back to main menu"
|
||||
echo ""
|
||||
echo -n "Choice [0-2]: "
|
||||
}
|
||||
|
||||
main() {
|
||||
while true; do
|
||||
show_menu
|
||||
read -r choice
|
||||
echo ""
|
||||
|
||||
case $choice in
|
||||
1) install_pterodactyl ;;
|
||||
2) show_status ;;
|
||||
0) return 75 ;;
|
||||
*) echo -e "${RED}Invalid option${NC}" ;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
read -p "Press Enter to continue..."
|
||||
done
|
||||
}
|
||||
|
||||
main
|
||||
Executable
+256
@@ -0,0 +1,256 @@
|
||||
#!/bin/bash
|
||||
|
||||
# LXS - Uptime Kuma Installation Script
|
||||
# Description: Install and manage Uptime Kuma monitoring tool
|
||||
# Author: LXS
|
||||
# Date: 2025
|
||||
|
||||
# Load LXS common library (colors, separator, run_spinner, loggers, helpers)
|
||||
LXS_RAW_BASE="${LXS_RAW_BASE:-https://git.hyko.cx/hykocx/lxs/raw/branch/main}"
|
||||
_lib=$(curl -fsSL "${LXS_RAW_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
|
||||
eval "$_lib"
|
||||
unset _lib
|
||||
export LXS_LOG_FILE="/tmp/lxs_uptime_kuma.log"
|
||||
|
||||
require_root "$0" "$@"
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Configuration
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
INSTALL_DIR="/opt/uptime-kuma"
|
||||
SERVICE_NAME="uptime-kuma"
|
||||
PORT=3001
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Helper Functions
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
is_installed() {
|
||||
[ -d "$INSTALL_DIR" ] && systemctl list-unit-files | grep -q "$SERVICE_NAME.service"
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Installation Functions
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
install_dependencies() {
|
||||
apt_noninteractive
|
||||
|
||||
run_spinner "Updating system..." "apt update -qq && apt upgrade -y -qq -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold'"
|
||||
run_spinner "Installing dependencies..." "apt install -y -qq curl git wget -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold'"
|
||||
run_spinner "Adding Node.js repository..." "curl -fsSL https://deb.nodesource.com/setup_20.x | bash -"
|
||||
run_spinner "Installing Node.js..." "apt install -y -qq nodejs -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold'"
|
||||
}
|
||||
|
||||
configure_domain_ssl() {
|
||||
local domain=$1
|
||||
local email=$2
|
||||
|
||||
run_spinner "Installing Nginx..." "apt install -y -qq nginx -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold'"
|
||||
run_spinner "Installing Certbot..." "apt install -y -qq certbot python3-certbot-nginx -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold'"
|
||||
|
||||
cat > /etc/nginx/sites-available/$domain <<EOF
|
||||
server {
|
||||
listen 80;
|
||||
server_name $domain;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:$PORT;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade \$http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host \$host;
|
||||
proxy_set_header X-Real-IP \$remote_addr;
|
||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
ln -sf /etc/nginx/sites-available/$domain /etc/nginx/sites-enabled/
|
||||
rm -f /etc/nginx/sites-enabled/default
|
||||
|
||||
nginx -t && systemctl restart nginx
|
||||
certbot --nginx -d $domain --non-interactive --agree-tos --email $email --redirect 2>/dev/null
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Main Installation
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
install_uptime_kuma() {
|
||||
local start_time=$(date +%s)
|
||||
|
||||
# Configure non-interactive mode globally
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
export NEEDRESTART_MODE=a
|
||||
export NEEDRESTART_SUSPEND=1
|
||||
|
||||
echo -e "${WHITE}${BOLD}UPTIME KUMA INSTALLATION${NC}\n"
|
||||
|
||||
if is_installed; then
|
||||
echo -e "${YELLOW}Uptime Kuma is already installed!${NC}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
require_disk_space 1024 || return 1
|
||||
|
||||
echo "[1/4] Installing dependencies..."
|
||||
install_dependencies
|
||||
echo ""
|
||||
|
||||
echo "[2/4] Cloning repository..."
|
||||
run_spinner "Downloading from GitHub..." "git clone https://github.com/louislam/uptime-kuma.git '$INSTALL_DIR'"
|
||||
echo ""
|
||||
|
||||
echo "[3/4] Installing Uptime Kuma..."
|
||||
cd "$INSTALL_DIR"
|
||||
run_spinner "Running npm setup..." "npm run setup"
|
||||
echo ""
|
||||
|
||||
echo "[4/4] Creating service..."
|
||||
cat > /etc/systemd/system/$SERVICE_NAME.service <<EOF
|
||||
[Unit]
|
||||
Description=Uptime Kuma
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=$INSTALL_DIR
|
||||
ExecStart=/usr/bin/npm run start-server
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
run_spinner "Starting service..." "systemctl daemon-reload && systemctl enable $SERVICE_NAME && systemctl start $SERVICE_NAME"
|
||||
sleep 5
|
||||
|
||||
local duration=$(( $(date +%s) - start_time ))
|
||||
local server_ip=$(get_public_ip)
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}${BOLD}Installation Completed!${NC}"
|
||||
echo -e "${GRAY}Time: $((duration / 60))m $((duration % 60))s${NC}"
|
||||
echo ""
|
||||
echo -e "${CYAN}Access URL: ${BOLD}http://$server_ip:$PORT${NC}"
|
||||
echo ""
|
||||
|
||||
ufw_allow "${PORT}/tcp" "Uptime Kuma"
|
||||
|
||||
# Optional domain configuration
|
||||
echo -e "${WHITE}Configure domain with SSL? (y/n)${NC}"
|
||||
read -n 1 -r
|
||||
echo ""
|
||||
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
read -p "Domain name: " DOMAIN_NAME
|
||||
read -p "Email for SSL: " EMAIL
|
||||
echo ""
|
||||
configure_domain_ssl "$DOMAIN_NAME" "$EMAIL"
|
||||
if [ $? -eq 0 ]; then
|
||||
echo -e "${GREEN}[✓] Domain configured: ${BOLD}https://$DOMAIN_NAME${NC}"
|
||||
ufw_allow 80/tcp "Uptime Kuma HTTP (nginx)"
|
||||
ufw_allow 443/tcp "Uptime Kuma HTTPS (nginx)"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${WHITE}Next steps:${NC}"
|
||||
echo -e "1. Visit the URL above"
|
||||
echo -e "2. Choose SQLite database"
|
||||
echo -e "3. Create admin account"
|
||||
}
|
||||
|
||||
update_uptime_kuma() {
|
||||
echo -e "${WHITE}${BOLD}UPTIME KUMA UPDATE${NC}\n"
|
||||
|
||||
# Configure non-interactive mode
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
export NEEDRESTART_MODE=a
|
||||
export NEEDRESTART_SUSPEND=1
|
||||
|
||||
if ! is_installed; then
|
||||
echo -e "${RED}Uptime Kuma is not installed!${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local start_time=$(date +%s)
|
||||
|
||||
run_spinner "Stopping service..." "systemctl stop $SERVICE_NAME"
|
||||
|
||||
BACKUP_DIR="${INSTALL_DIR}_backup_$(date +%Y%m%d_%H%M%S)"
|
||||
[ -d "$INSTALL_DIR/data" ] && run_spinner "Backing up..." "cp -r '$INSTALL_DIR/data' '$BACKUP_DIR'"
|
||||
|
||||
cd "$INSTALL_DIR"
|
||||
run_spinner "Pulling updates..." "git fetch --all && git checkout master && git pull"
|
||||
run_spinner "Updating dependencies..." "npm run setup"
|
||||
run_spinner "Starting service..." "systemctl start $SERVICE_NAME"
|
||||
|
||||
local duration=$(( $(date +%s) - start_time ))
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}${BOLD}Update Completed!${NC}"
|
||||
echo -e "${GRAY}Time: $((duration / 60))m $((duration % 60))s${NC}"
|
||||
echo -e "${CYAN}Access: ${BOLD}http://$(get_public_ip):$PORT${NC}"
|
||||
}
|
||||
|
||||
show_status() {
|
||||
echo -e "${WHITE}${BOLD}UPTIME KUMA STATUS${NC}\n"
|
||||
|
||||
if ! is_installed; then
|
||||
echo -e "${RED}Uptime Kuma is not installed!${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if systemctl is-active --quiet $SERVICE_NAME; then
|
||||
echo -e "${GREEN}Service is running${NC}\n"
|
||||
systemctl status $SERVICE_NAME --no-pager | head -10
|
||||
echo ""
|
||||
echo -e "${CYAN}Access: ${BOLD}http://$(get_public_ip):$PORT${NC}"
|
||||
else
|
||||
echo -e "${RED}Service is not running${NC}"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Main Menu
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
show_menu() {
|
||||
clear
|
||||
echo -e "${WHITE}${BOLD}UPTIME KUMA MANAGEMENT${NC}\n"
|
||||
echo -e " ${GREEN}[1]${NC} Install Uptime Kuma"
|
||||
echo -e " ${YELLOW}[2]${NC} Update Uptime Kuma"
|
||||
echo -e " ${CYAN}[3]${NC} View Status"
|
||||
echo -e " ${RED}[0]${NC} Back to main menu"
|
||||
echo ""
|
||||
echo -n "Choice [0-3]: "
|
||||
}
|
||||
|
||||
main() {
|
||||
while true; do
|
||||
show_menu
|
||||
read -r choice
|
||||
echo ""
|
||||
|
||||
case $choice in
|
||||
1) install_uptime_kuma ;;
|
||||
2) update_uptime_kuma ;;
|
||||
3) show_status ;;
|
||||
0) return 75 ;;
|
||||
*) echo -e "${RED}Invalid option${NC}" ;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
read -p "Press Enter to continue..."
|
||||
done
|
||||
}
|
||||
|
||||
main
|
||||
@@ -0,0 +1,294 @@
|
||||
#!/bin/bash
|
||||
|
||||
# LXS - Common library
|
||||
# Sourced by lxs.sh and all sub-scripts. Provides colors, UI helpers, loggers,
|
||||
# spinner, OS guards, and shared utilities (public IP, password generation,
|
||||
# apt lock wait, root re-exec, apt non-interactive setup).
|
||||
# Repo: https://git.hyko.cx/hykocx/lxs
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Colors — minimal palette: red, cyan, white (+ gray as a white shade)
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
RED='\033[38;2;255;64;64m' # errors, destructive actions
|
||||
CYAN='\033[38;2;0;229;255m' # accents, titles, OK, info
|
||||
WHITE='\033[38;2;240;240;240m' # primary text
|
||||
GRAY='\033[38;2;140;140;140m' # secondary text, comments, separators
|
||||
NC='\033[0m'
|
||||
BOLD='\033[1m'
|
||||
DIM='\033[2m'
|
||||
|
||||
# Legacy aliases — older sub-scripts still reference these. Map onto CYAN
|
||||
# so the palette stays at red/cyan/white without breaking them.
|
||||
YELLOW="$CYAN"
|
||||
GREEN="$CYAN"
|
||||
MAGENTA="$CYAN"
|
||||
PURPLE="$CYAN"
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# UI helpers — title + horizontal rule, no closed box.
|
||||
#
|
||||
# Layout:
|
||||
# show_box_top "TITLE" ["RIGHT_TAG"] → TITLE [ RIGHT ]
|
||||
# ─────────────────────────────────
|
||||
# show_box_mid "SECTION" → ─ SECTION ─────────────────────
|
||||
# show_box_bottom → ───────────────────────────────
|
||||
# show_separator → ─── (light gray divider)
|
||||
# show_menu_item "01" "LABEL" "desc" → [01] LABEL // desc
|
||||
# show_prompt → >
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
_lxs_term_cols() {
|
||||
local cols
|
||||
cols=$(tput cols 2>/dev/null || echo 80)
|
||||
[ "$cols" -gt 100 ] && cols=100
|
||||
[ "$cols" -lt 60 ] && cols=60
|
||||
echo "$cols"
|
||||
}
|
||||
|
||||
show_box_top() {
|
||||
local title="$1" right="${2:-}"
|
||||
local cols pad_len pad
|
||||
cols=$(_lxs_term_cols)
|
||||
if [ -n "$right" ]; then
|
||||
pad_len=$(( cols - 2 - ${#title} - ${#right} - 4 ))
|
||||
[ "$pad_len" -lt 1 ] && pad_len=1
|
||||
pad=$(printf ' %.0s' $(seq 1 "$pad_len"))
|
||||
printf " ${CYAN}${BOLD}%s${NC}%s${GRAY}[ ${CYAN}%s${GRAY} ]${NC}\n" \
|
||||
"$title" "$pad" "$right"
|
||||
else
|
||||
printf " ${CYAN}${BOLD}%s${NC}\n" "$title"
|
||||
fi
|
||||
_lxs_hr "$cols"
|
||||
}
|
||||
|
||||
show_box_mid() {
|
||||
local title="$1"
|
||||
local cols fill_len fill
|
||||
cols=$(_lxs_term_cols)
|
||||
fill_len=$(( cols - ${#title} - 4 ))
|
||||
[ "$fill_len" -lt 2 ] && fill_len=2
|
||||
fill=$(printf '─%.0s' $(seq 1 "$fill_len"))
|
||||
printf "${GRAY}─ ${CYAN}${BOLD}%s${NC} ${GRAY}%s${NC}\n" "$title" "$fill"
|
||||
}
|
||||
|
||||
show_box_bottom() {
|
||||
_lxs_hr "$(_lxs_term_cols)"
|
||||
}
|
||||
|
||||
show_separator() {
|
||||
_lxs_hr "$(_lxs_term_cols)"
|
||||
}
|
||||
|
||||
_lxs_hr() {
|
||||
local cols=$1 fill
|
||||
fill=$(printf '─%.0s' $(seq 1 "$cols"))
|
||||
printf "${GRAY}%s${NC}\n" "$fill"
|
||||
}
|
||||
|
||||
show_title() {
|
||||
local title="$1"
|
||||
local subtitle="${2:-}"
|
||||
echo ""
|
||||
if [ -n "$subtitle" ]; then
|
||||
show_box_top "$title" "$subtitle"
|
||||
else
|
||||
show_box_top "$title"
|
||||
fi
|
||||
}
|
||||
|
||||
# Render a menu line. Pass "exit" as 4th arg to color the key red.
|
||||
# show_menu_item "01" "APPLICATIONS" "deploy stacks"
|
||||
# show_menu_item "00" "EXIT" "quit shell" exit
|
||||
show_menu_item() {
|
||||
local key="$1" label="$2" desc="${3:-}" kind="${4:-}"
|
||||
local key_color="${CYAN}"
|
||||
[ "$kind" = "exit" ] && key_color="${RED}"
|
||||
if [ -n "$desc" ]; then
|
||||
printf " ${key_color}[%s]${NC} ${WHITE}${BOLD}%-18s${NC} ${GRAY}// %s${NC}\n" \
|
||||
"$key" "$label" "$desc"
|
||||
else
|
||||
printf " ${key_color}[%s]${NC} ${WHITE}${BOLD}%s${NC}\n" "$key" "$label"
|
||||
fi
|
||||
}
|
||||
|
||||
show_prompt() {
|
||||
echo -e -n " ${CYAN}>${NC} "
|
||||
}
|
||||
|
||||
info() { echo -e "${CYAN}[..]${NC} $*"; }
|
||||
ok() { echo -e "${CYAN}[OK]${NC} $*"; }
|
||||
warn() { echo -e "${CYAN}[!!]${NC} $*"; }
|
||||
err() { echo -e "${RED}[KO]${NC} $*" >&2; }
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Spinner — runs a shell command, redirects output to a log file, shows
|
||||
# a spinner with a label, prints ✓/✗ on completion. Returns the command's
|
||||
# exit code.
|
||||
#
|
||||
# Usage:
|
||||
# run_spinner "Installing foo..." "apt-get install -y foo"
|
||||
#
|
||||
# Log file: ${LXS_LOG_FILE:-/tmp/lxs.log}
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
run_spinner() {
|
||||
local message=$1
|
||||
shift
|
||||
local command="$*"
|
||||
local spinstr='|/-\'
|
||||
local log="${LXS_LOG_FILE:-/tmp/lxs.log}"
|
||||
|
||||
echo -e "${CYAN}[..] ${message}${NC}"
|
||||
eval "$command" > "$log" 2>&1 &
|
||||
local pid=$!
|
||||
|
||||
while kill -0 "$pid" 2>/dev/null; do
|
||||
local temp=${spinstr#?}
|
||||
printf "\r${CYAN}[%c.]${NC} ${message}" "$spinstr"
|
||||
spinstr=$temp${spinstr%"$temp"}
|
||||
sleep 0.15
|
||||
done
|
||||
|
||||
wait "$pid"
|
||||
local exit_code=$?
|
||||
if [ $exit_code -eq 0 ]; then
|
||||
printf "\r${CYAN}[OK]${NC} ${message}\n"
|
||||
else
|
||||
printf "\r${RED}[KO]${NC} ${message}\n"
|
||||
fi
|
||||
return $exit_code
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Guards
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
require_debian_ubuntu() {
|
||||
if ! grep -qiE 'debian|ubuntu' /etc/os-release 2>/dev/null; then
|
||||
err "This script supports Debian/Ubuntu only."
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Verify enough free disk space before doing apt installs or writing config.
|
||||
# Fails early with a clear message so we don't get half-installed packages or
|
||||
# files truncated mid-write when the disk is full.
|
||||
#
|
||||
# Usage:
|
||||
# require_disk_space # 500MB on /var (apt cache + lists)
|
||||
# require_disk_space 1024 # 1024MB on /var
|
||||
# require_disk_space 1024 /var /opt # 1024MB on each listed path
|
||||
#
|
||||
# Duplicate filesystems (e.g. /var on the same fs as /) are checked once.
|
||||
require_disk_space() {
|
||||
local min_mb=${1:-500}
|
||||
shift 2>/dev/null || true
|
||||
local paths=("$@")
|
||||
[ ${#paths[@]} -eq 0 ] && paths=(/var)
|
||||
|
||||
local path avail fs seen=" " rc=0
|
||||
for path in "${paths[@]}"; do
|
||||
fs=$(df -P "$path" 2>/dev/null | awk 'NR==2 {print $1}')
|
||||
if [ -z "$fs" ]; then
|
||||
err "Cannot read disk usage for ${path}"
|
||||
rc=1
|
||||
continue
|
||||
fi
|
||||
[[ "$seen" == *" $fs "* ]] && continue
|
||||
seen="$seen$fs "
|
||||
|
||||
avail=$(df -Pm "$path" 2>/dev/null | awk 'NR==2 {print $4}')
|
||||
if [ "${avail:-0}" -lt "$min_mb" ]; then
|
||||
err "Not enough disk space on ${path} (${fs}): ${avail}MB free, ${min_mb}MB required."
|
||||
rc=1
|
||||
fi
|
||||
done
|
||||
return $rc
|
||||
}
|
||||
|
||||
# Re-exec the current script via sudo if not already root. Preserves env and
|
||||
# arguments. Call near the top of a sub-script:
|
||||
# require_root "$0" "$@"
|
||||
require_root() {
|
||||
[ "$EUID" -eq 0 ] && return 0
|
||||
local self=$1
|
||||
shift
|
||||
exec sudo -E "$self" "$@"
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Network / random / apt helpers
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
get_public_ip() {
|
||||
local ip=""
|
||||
ip=$(curl -s --max-time 5 ifconfig.me 2>/dev/null) || \
|
||||
ip=$(curl -s --max-time 5 icanhazip.com 2>/dev/null) || \
|
||||
ip=$(curl -s --max-time 5 ipinfo.io/ip 2>/dev/null) || \
|
||||
ip=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||||
echo "$ip"
|
||||
}
|
||||
|
||||
# Generate a random alphanumeric string. Default length: 32.
|
||||
generate_password() {
|
||||
local length=${1:-32}
|
||||
LC_ALL=C tr -dc 'a-zA-Z0-9' </dev/urandom | head -c "$length"
|
||||
}
|
||||
|
||||
# Configure apt/debconf for fully non-interactive runs. Safe to call multiple
|
||||
# times. No-op when apt is absent.
|
||||
apt_noninteractive() {
|
||||
command -v apt >/dev/null 2>&1 || return 0
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
export NEEDRESTART_MODE=a
|
||||
export NEEDRESTART_SUSPEND=1
|
||||
echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Add a UFW allow rule, but only if UFW is installed AND active. No-op
|
||||
# otherwise — so app installers can declare the ports they need without
|
||||
# forcing UFW on hosts that don't use it.
|
||||
#
|
||||
# Usage:
|
||||
# ufw_allow 8000/tcp "Coolify dashboard"
|
||||
# ufw_allow 80/tcp
|
||||
ufw_allow() {
|
||||
command -v ufw >/dev/null 2>&1 || return 0
|
||||
ufw status 2>/dev/null | grep -q "Status: active" || return 0
|
||||
local rule=$1 comment=${2:-}
|
||||
if [ -n "$comment" ]; then
|
||||
ufw allow "$rule" comment "$comment" >/dev/null
|
||||
else
|
||||
ufw allow "$rule" >/dev/null
|
||||
fi
|
||||
ok "UFW :: allowed ${rule}${comment:+ (${comment})}"
|
||||
}
|
||||
|
||||
# Wait for other apt/dpkg processes to release their locks. Up to 120s.
|
||||
wait_for_apt() {
|
||||
command -v apt >/dev/null 2>&1 || return 0
|
||||
|
||||
local max_wait=120
|
||||
local wait_count=0
|
||||
local lock_detected=false
|
||||
|
||||
while fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1 || \
|
||||
fuser /var/lib/dpkg/lock >/dev/null 2>&1 || \
|
||||
fuser /var/lib/apt/lists/lock >/dev/null 2>&1; do
|
||||
|
||||
[ "$lock_detected" = false ] && lock_detected=true && \
|
||||
echo -e "${YELLOW}[!!] Waiting for other package manager to finish...${NC}"
|
||||
|
||||
wait_count=$((wait_count + 1))
|
||||
[ $wait_count -ge $max_wait ] && \
|
||||
echo -e "${RED}[KO] Timeout waiting for package manager${NC}" && return 1
|
||||
|
||||
printf "\r${GRAY}[..] Waiting... (%ds/%ds)${NC}" $wait_count $max_wait
|
||||
sleep 1
|
||||
done
|
||||
|
||||
[ "$lock_detected" = true ] && \
|
||||
echo -e "\n${GREEN}[OK] Package manager is now available${NC}"
|
||||
return 0
|
||||
}
|
||||
Executable
+422
@@ -0,0 +1,422 @@
|
||||
#!/bin/bash
|
||||
|
||||
# LXS - Linux multi-tool
|
||||
# Description: Centralized server management and deployment toolkit
|
||||
# Repo: https://git.hyko.cx/hykocx/lxs
|
||||
# License: MIT
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Configuration
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
LXS_SCRIPT_DIR=""
|
||||
if [ -n "${BASH_SOURCE[0]:-}" ]; then
|
||||
_lxs_resolved=$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null) \
|
||||
|| _lxs_resolved=$(realpath "${BASH_SOURCE[0]}" 2>/dev/null) \
|
||||
|| _lxs_resolved="${BASH_SOURCE[0]}"
|
||||
LXS_SCRIPT_DIR=$(dirname "$_lxs_resolved")
|
||||
unset _lxs_resolved
|
||||
fi
|
||||
if [ -n "$LXS_SCRIPT_DIR" ] && [ -r "${LXS_SCRIPT_DIR}/VERSION" ]; then
|
||||
LXS_VERSION=$(head -n1 "${LXS_SCRIPT_DIR}/VERSION" 2>/dev/null | tr -d '[:space:]')
|
||||
fi
|
||||
LXS_VERSION="${LXS_VERSION:-dev}"
|
||||
|
||||
LXS_REPO_URL="https://git.hyko.cx/hykocx/lxs"
|
||||
LXS_BRANCH="${LXS_BRANCH:-main}"
|
||||
export LXS_RAW_BASE="${LXS_RAW_BASE:-${LXS_REPO_URL}/raw/branch/${LXS_BRANCH}}"
|
||||
LXS_TARBALL_URL="${LXS_TARBALL_URL:-${LXS_REPO_URL}/archive/${LXS_BRANCH}.tar.gz}"
|
||||
|
||||
LXS_INSTALL_DIR="${LXS_INSTALL_DIR:-/usr/local/share/lxs}"
|
||||
LXS_BIN_PATH="${LXS_BIN_PATH:-/usr/local/bin/lxs}"
|
||||
LXS_CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/lxs"
|
||||
LXS_VERSION_CACHE="${LXS_CACHE_DIR}/remote_version"
|
||||
LXS_VERSION_TTL=86400
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Load common library
|
||||
# Prefers a sibling lib/common.sh (installed or repo checkout); falls back to
|
||||
# fetching it from the remote when run via `curl | bash`.
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
if [ -n "$LXS_SCRIPT_DIR" ] && [ -r "${LXS_SCRIPT_DIR}/lib/common.sh" ]; then
|
||||
# shellcheck disable=SC1091
|
||||
. "${LXS_SCRIPT_DIR}/lib/common.sh"
|
||||
else
|
||||
_lib=$(curl -fsSL "${LXS_RAW_BASE}/lib/common.sh") || {
|
||||
echo "Failed to fetch lib/common.sh" >&2
|
||||
exit 1
|
||||
}
|
||||
eval "$_lib"
|
||||
unset _lib
|
||||
fi
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Core helpers
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
lxs_self_path() {
|
||||
local src="${BASH_SOURCE[0]}"
|
||||
[ -z "$src" ] && return 1
|
||||
if command -v readlink >/dev/null 2>&1; then
|
||||
readlink -f "$src" 2>/dev/null && return 0
|
||||
fi
|
||||
if command -v realpath >/dev/null 2>&1; then
|
||||
realpath "$src" 2>/dev/null && return 0
|
||||
fi
|
||||
echo "$src"
|
||||
}
|
||||
|
||||
lxs_is_installed() {
|
||||
local self
|
||||
self=$(lxs_self_path) || return 1
|
||||
[ "$(dirname "$self")" = "$LXS_INSTALL_DIR" ]
|
||||
}
|
||||
|
||||
lxs_need_root_for() {
|
||||
local path=$1
|
||||
local parent
|
||||
parent=$(dirname "$path")
|
||||
[ -w "$parent" ] && return 1
|
||||
[ "$EUID" -eq 0 ] && return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
check_remote_version() {
|
||||
LXS_UPDATE_AVAILABLE=0
|
||||
LXS_REMOTE_VERSION=""
|
||||
|
||||
mkdir -p "$LXS_CACHE_DIR" 2>/dev/null || return 0
|
||||
|
||||
local now mtime age=999999
|
||||
now=$(date +%s 2>/dev/null) || return 0
|
||||
if [ -f "$LXS_VERSION_CACHE" ]; then
|
||||
mtime=$(stat -c %Y "$LXS_VERSION_CACHE" 2>/dev/null || stat -f %m "$LXS_VERSION_CACHE" 2>/dev/null || echo 0)
|
||||
age=$((now - mtime))
|
||||
fi
|
||||
|
||||
if [ "$age" -ge "$LXS_VERSION_TTL" ]; then
|
||||
(
|
||||
curl -fsSL --max-time 3 -H "Cache-Control: no-cache" \
|
||||
"${LXS_RAW_BASE}/VERSION" -o "${LXS_VERSION_CACHE}.tmp" 2>/dev/null \
|
||||
&& mv "${LXS_VERSION_CACHE}.tmp" "$LXS_VERSION_CACHE" \
|
||||
|| rm -f "${LXS_VERSION_CACHE}.tmp"
|
||||
) >/dev/null 2>&1 &
|
||||
disown 2>/dev/null || true
|
||||
fi
|
||||
|
||||
[ -f "$LXS_VERSION_CACHE" ] || return 0
|
||||
|
||||
local cached
|
||||
cached=$(head -n1 "$LXS_VERSION_CACHE" 2>/dev/null | tr -d '[:space:]')
|
||||
[ -z "$cached" ] && return 0
|
||||
|
||||
local highest
|
||||
highest=$(printf '%s\n%s\n' "$LXS_VERSION" "$cached" | sort -V | tail -n1)
|
||||
if [ "$highest" = "$cached" ] && [ "$cached" != "$LXS_VERSION" ]; then
|
||||
LXS_UPDATE_AVAILABLE=1
|
||||
LXS_REMOTE_VERSION="$cached"
|
||||
fi
|
||||
}
|
||||
|
||||
show_lxs_logo() {
|
||||
show_box_top "LXS // v${LXS_VERSION}" "ONLINE"
|
||||
}
|
||||
|
||||
show_header() {
|
||||
clear
|
||||
show_lxs_logo
|
||||
|
||||
if [ "${LXS_UPDATE_AVAILABLE:-0}" = "1" ]; then
|
||||
echo ""
|
||||
echo -e " ${CYAN}[!!] UPDATE_AVAILABLE${NC} ${WHITE}v${LXS_REMOTE_VERSION}${NC} ${GRAY}// run \`lxs update\`${NC}"
|
||||
fi
|
||||
|
||||
local hostname os_version kernel uptime_info ip_address cpu_cores total_mem used_mem disk_usage
|
||||
hostname=$(hostname 2>/dev/null || cat /etc/hostname 2>/dev/null || echo "${HOSTNAME:-unknown}")
|
||||
os_version=$(lsb_release -ds 2>/dev/null || grep PRETTY_NAME /etc/os-release 2>/dev/null | cut -d'"' -f2)
|
||||
kernel=$(uname -r)
|
||||
uptime_info=$(uptime -p 2>/dev/null | sed 's/^up //' || uptime | awk -F'up ' '{print $2}' | awk -F',' '{print $1}')
|
||||
ip_address=$(get_public_ip)
|
||||
cpu_cores=$(nproc)
|
||||
total_mem=$(free -h | awk '/^Mem:/ {print $2}')
|
||||
used_mem=$(free -h | awk '/^Mem:/ {print $3}')
|
||||
disk_usage=$(df -h / | awk 'NR==2 {print $3 "/" $2 " (" $5 ")"}')
|
||||
|
||||
echo ""
|
||||
printf " ${CYAN}NODE${NC} ${WHITE}%-26s${NC} ${CYAN}KERN${NC} ${WHITE}%s${NC}\n" "$hostname" "$kernel"
|
||||
printf " ${CYAN}ADDR${NC} ${WHITE}%-26s${NC} ${CYAN}UP${NC} ${WHITE}%s${NC}\n" "$ip_address" "$uptime_info"
|
||||
printf " ${CYAN}OS${NC} ${WHITE}%-26s${NC} ${CYAN}CPU${NC} ${WHITE}%s cores${NC}\n" "$os_version" "$cpu_cores"
|
||||
printf " ${CYAN}DISK${NC} ${WHITE}%-26s${NC} ${CYAN}MEM${NC} ${WHITE}%s / %s${NC}\n" "$disk_usage" "$used_mem" "$total_mem"
|
||||
echo ""
|
||||
}
|
||||
|
||||
check_os() {
|
||||
if ! grep -qiE 'debian|ubuntu' /etc/os-release 2>/dev/null; then
|
||||
echo -e "${YELLOW}[!!] LXS is tested on Debian/Ubuntu. Other distros may not work.${NC}"
|
||||
read -r -p "Continue anyway? [y/N] " reply
|
||||
case "$reply" in
|
||||
[yY]|[yY][eE][sS]) ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
fi
|
||||
}
|
||||
|
||||
download_and_run() {
|
||||
local script_path=$1
|
||||
shift
|
||||
local script_name local_path
|
||||
script_name=$(basename "$script_path")
|
||||
local_path="${LXS_INSTALL_DIR}/${script_path}"
|
||||
|
||||
if lxs_is_installed && [ -f "$local_path" ]; then
|
||||
chmod +x "$local_path" 2>/dev/null || true
|
||||
echo ""
|
||||
show_separator
|
||||
echo ""
|
||||
"$local_path" "$@"
|
||||
local exit_code=$?
|
||||
echo ""
|
||||
show_separator
|
||||
if [ $exit_code -eq 0 ]; then
|
||||
echo -e "${GREEN}[OK] Script completed successfully${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}[!!] Script exited with code: $exit_code${NC}"
|
||||
fi
|
||||
return $exit_code
|
||||
fi
|
||||
|
||||
local temp_file
|
||||
temp_file=$(mktemp "/tmp/lxs.${script_name%.*}.XXXXXX.sh")
|
||||
|
||||
echo ""
|
||||
echo -e "${CYAN}[..] Fetching ${BOLD}${script_name}${NC}${CYAN}...${NC}"
|
||||
|
||||
if curl -fsSL -H "Cache-Control: no-cache" \
|
||||
-o "${temp_file}" \
|
||||
"${LXS_RAW_BASE}/${script_path}"; then
|
||||
echo -e "${GREEN}[OK] Payload acquired${NC}"
|
||||
chmod +x "${temp_file}"
|
||||
|
||||
echo ""
|
||||
show_separator
|
||||
echo ""
|
||||
|
||||
"${temp_file}" "$@"
|
||||
local exit_code=$?
|
||||
|
||||
rm -f "${temp_file}"
|
||||
|
||||
echo ""
|
||||
show_separator
|
||||
if [ $exit_code -eq 0 ]; then
|
||||
echo -e "${GREEN}[OK] Script completed successfully${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}[!!] Script exited with code: $exit_code${NC}"
|
||||
fi
|
||||
return $exit_code
|
||||
else
|
||||
echo -e "${RED}[KO] Failed to download ${script_path}${NC}"
|
||||
echo -e "${RED} URL: ${LXS_RAW_BASE}/${script_path}${NC}"
|
||||
rm -f "${temp_file}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# CLI commands
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
cmd_version() {
|
||||
echo "lxs ${LXS_VERSION}"
|
||||
}
|
||||
|
||||
cmd_help() {
|
||||
cat <<EOF
|
||||
LXS - Linux multi-tool (v${LXS_VERSION})
|
||||
|
||||
Usage:
|
||||
lxs Interactive menu (browse apps and tools)
|
||||
lxs setup Install lxs and all sub-scripts to ${LXS_INSTALL_DIR}
|
||||
lxs update Update all installed files to latest
|
||||
lxs install <app> Install an application
|
||||
lxs tool <name> [args] Run a system tool
|
||||
lxs info Show system info
|
||||
lxs version Show version
|
||||
lxs help Show this help
|
||||
|
||||
Run \`lxs\` for the interactive menu to browse available applications and tools.
|
||||
|
||||
Source: ${LXS_REPO_URL}
|
||||
EOF
|
||||
}
|
||||
|
||||
cmd_install() {
|
||||
local app="${1:-}"
|
||||
case "$app" in
|
||||
coolify) download_and_run "apps/coolify.sh" ;;
|
||||
pterodactyl) download_and_run "apps/pterodactyl.sh" ;;
|
||||
uptime-kuma) download_and_run "apps/uptime-kuma.sh" ;;
|
||||
cloudpanel) download_and_run "apps/cloudpanel.sh" ;;
|
||||
proxmox) download_and_run "apps/proxmox.sh" ;;
|
||||
"") echo -e "${RED}[KO] Missing app name. Try: lxs help${NC}"; return 1 ;;
|
||||
*) echo -e "${RED}[KO] Unknown app: $app. Try: lxs help${NC}"; return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
cmd_tool() {
|
||||
local tool="${1:-}"
|
||||
shift || true
|
||||
case "$tool" in
|
||||
system) download_and_run "tools/system-infos.sh" "$@" ;;
|
||||
benchmark) download_and_run "tools/server-benchmark.sh" "$@" ;;
|
||||
harden) download_and_run "tools/harden.sh" "$@" ;;
|
||||
root-password) download_and_run "tools/root-password.sh" "$@" ;;
|
||||
update) download_and_run "tools/update-server.sh" "$@" ;;
|
||||
root-ssh-login) download_and_run "tools/root-ssh-login.sh" "$@" ;;
|
||||
welcome|motd) download_and_run "tools/welcome-message.sh" "$@" ;;
|
||||
"") echo -e "${RED}[KO] Missing tool name. Try: lxs help${NC}"; return 1 ;;
|
||||
*) echo -e "${RED}[KO] Unknown tool: $tool. Try: lxs help${NC}"; return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
lxs_sync_install() {
|
||||
local action="${1:-update}"
|
||||
|
||||
if lxs_need_root_for "$LXS_INSTALL_DIR" || lxs_need_root_for "$LXS_BIN_PATH"; then
|
||||
echo -e "${YELLOW}[!!] Need sudo to write ${LXS_INSTALL_DIR}; re-running with sudo...${NC}"
|
||||
exec sudo -E LXS_INSTALL_DIR="$LXS_INSTALL_DIR" LXS_BIN_PATH="$LXS_BIN_PATH" \
|
||||
LXS_BRANCH="$LXS_BRANCH" LXS_REPO_URL="$LXS_REPO_URL" \
|
||||
bash "$(lxs_self_path)" "$action"
|
||||
fi
|
||||
|
||||
local work
|
||||
work=$(mktemp -d /tmp/lxs.install.XXXXXX) || {
|
||||
echo -e "${RED}[KO] Failed to create temp dir${NC}"
|
||||
return 1
|
||||
}
|
||||
trap 'rm -rf "$work"' RETURN
|
||||
|
||||
echo -e "${CYAN}[..] Fetching ${LXS_TARBALL_URL}...${NC}"
|
||||
if ! curl -fsSL -H "Cache-Control: no-cache" -o "${work}/lxs.tgz" "$LXS_TARBALL_URL"; then
|
||||
echo -e "${RED}[KO] Download failed${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
mkdir -p "${work}/extracted"
|
||||
if ! tar -xzf "${work}/lxs.tgz" --strip-components=1 -C "${work}/extracted"; then
|
||||
echo -e "${RED}[KO] Extraction failed${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ ! -f "${work}/extracted/lxs.sh" ] || [ ! -f "${work}/extracted/VERSION" ]; then
|
||||
echo -e "${RED}[KO] Tarball is missing lxs.sh or VERSION${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
mkdir -p "$LXS_INSTALL_DIR"
|
||||
|
||||
if command -v rsync >/dev/null 2>&1; then
|
||||
rsync -a --delete "${work}/extracted/" "${LXS_INSTALL_DIR}/" || {
|
||||
echo -e "${RED}[KO] rsync failed${NC}"
|
||||
return 1
|
||||
}
|
||||
else
|
||||
rm -rf "${LXS_INSTALL_DIR}.old" 2>/dev/null
|
||||
[ -d "$LXS_INSTALL_DIR" ] && mv "$LXS_INSTALL_DIR" "${LXS_INSTALL_DIR}.old"
|
||||
mkdir -p "$LXS_INSTALL_DIR"
|
||||
if ! cp -a "${work}/extracted/." "${LXS_INSTALL_DIR}/"; then
|
||||
echo -e "${RED}[KO] Copy failed; restoring previous install${NC}"
|
||||
rm -rf "$LXS_INSTALL_DIR"
|
||||
[ -d "${LXS_INSTALL_DIR}.old" ] && mv "${LXS_INSTALL_DIR}.old" "$LXS_INSTALL_DIR"
|
||||
return 1
|
||||
fi
|
||||
rm -rf "${LXS_INSTALL_DIR}.old"
|
||||
fi
|
||||
|
||||
chmod +x "${LXS_INSTALL_DIR}/lxs.sh" 2>/dev/null || true
|
||||
find "${LXS_INSTALL_DIR}/apps" "${LXS_INSTALL_DIR}/tools" -name '*.sh' -exec chmod +x {} + 2>/dev/null || true
|
||||
|
||||
if [ -e "$LXS_BIN_PATH" ] || [ -L "$LXS_BIN_PATH" ]; then
|
||||
rm -f "$LXS_BIN_PATH"
|
||||
fi
|
||||
ln -sfn "${LXS_INSTALL_DIR}/lxs.sh" "$LXS_BIN_PATH"
|
||||
|
||||
rm -f "$LXS_VERSION_CACHE"
|
||||
|
||||
local new_version
|
||||
new_version=$(head -n1 "${LXS_INSTALL_DIR}/VERSION" 2>/dev/null | tr -d '[:space:]')
|
||||
echo -e "${GREEN}[OK] lxs ${new_version} installed in ${LXS_INSTALL_DIR}${NC}"
|
||||
echo -e "${GREEN}[OK] Symlink: ${LXS_BIN_PATH} → ${LXS_INSTALL_DIR}/lxs.sh${NC}"
|
||||
}
|
||||
|
||||
cmd_update() {
|
||||
lxs_sync_install update
|
||||
}
|
||||
|
||||
cmd_install_self() {
|
||||
lxs_sync_install setup
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Interactive menu
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
main_menu() {
|
||||
check_os
|
||||
check_remote_version
|
||||
while true; do
|
||||
show_header
|
||||
show_box_mid "SELECT"
|
||||
echo ""
|
||||
show_menu_item "01" "APPLICATIONS" "deploy stacks"
|
||||
show_menu_item "02" "TOOLS" "sysadmin daemons"
|
||||
show_menu_item "UU" "FETCH_UPDATE" "sync remote"
|
||||
show_menu_item "00" "JACK_OUT" "exit shell" exit
|
||||
echo ""
|
||||
show_box_bottom
|
||||
echo ""
|
||||
show_prompt
|
||||
read -r choice
|
||||
|
||||
case $choice in
|
||||
1) download_and_run "apps/index.sh" ;;
|
||||
2) download_and_run "tools/index.sh" ;;
|
||||
u|U|uu|UU)
|
||||
if cmd_update && [ -x "${LXS_INSTALL_DIR}/lxs.sh" ]; then
|
||||
echo ""
|
||||
read -r -p "Press Enter to reload with the new version..."
|
||||
exec bash "${LXS_INSTALL_DIR}/lxs.sh"
|
||||
fi
|
||||
;;
|
||||
0|00)
|
||||
clear
|
||||
show_lxs_logo
|
||||
echo -e "${CYAN}JACK_OUT${NC} ${GRAY}// session terminated${NC}\n"
|
||||
exit 0
|
||||
;;
|
||||
*) echo -e "${RED}[KO] Invalid protocol.${NC}"; sleep 1 ;;
|
||||
esac
|
||||
|
||||
if [[ "$choice" != "0" && "$choice" != "00" && "$choice" != "1" && "$choice" != "2" ]]; then
|
||||
echo ""
|
||||
read -r -p "Press Enter to continue..."
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Entrypoint
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
case "${1:-}" in
|
||||
install) shift; cmd_install "$@" ;;
|
||||
tool) shift; cmd_tool "$@" ;;
|
||||
info) show_header ;;
|
||||
update) cmd_update ;;
|
||||
setup|install-self) cmd_install_self ;;
|
||||
version|-v|--version) cmd_version ;;
|
||||
help|-h|--help) cmd_help ;;
|
||||
"") main_menu ;;
|
||||
*) echo -e "${RED}[KO] Unknown command: $1${NC}"; cmd_help; exit 1 ;;
|
||||
esac
|
||||
Executable
+166
@@ -0,0 +1,166 @@
|
||||
#!/bin/bash
|
||||
|
||||
# LXS - Server Hardening
|
||||
# Description: Apply baseline security hardening (UFW + fail2ban + unattended-upgrades)
|
||||
# Author: LXS
|
||||
# Date: 2025
|
||||
|
||||
# Load LXS common library (colors, separator, run_spinner, loggers, helpers)
|
||||
LXS_RAW_BASE="${LXS_RAW_BASE:-https://git.hyko.cx/hykocx/lxs/raw/branch/main}"
|
||||
_lib=$(curl -fsSL "${LXS_RAW_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
|
||||
eval "$_lib"
|
||||
unset _lib
|
||||
export LXS_LOG_FILE="/tmp/lxs_harden.log"
|
||||
|
||||
require_root "$0" "$@"
|
||||
|
||||
set -u
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Configuration
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
DO_UFW=1
|
||||
DO_FAIL2BAN=1
|
||||
DO_UNATTENDED=1
|
||||
ASSUME_YES=0
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--no-ufw) DO_UFW=0 ;;
|
||||
--no-fail2ban) DO_FAIL2BAN=0 ;;
|
||||
--no-unattended) DO_UNATTENDED=0 ;;
|
||||
-y|--yes) ASSUME_YES=1 ;;
|
||||
-h|--help)
|
||||
cat <<EOF
|
||||
Usage: harden.sh [options]
|
||||
|
||||
Options:
|
||||
--no-ufw Skip UFW firewall setup
|
||||
--no-fail2ban Skip fail2ban setup
|
||||
--no-unattended Skip unattended-upgrades setup
|
||||
-y, --yes Skip confirmation prompt
|
||||
-h, --help Show this help
|
||||
EOF
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Unknown option: $arg${NC}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Pre-checks
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
require_debian_ubuntu || exit 1
|
||||
require_disk_space 500 || exit 1
|
||||
|
||||
# Read the effective Port from sshd_config + any drop-in under sshd_config.d/
|
||||
sshd_effective_port() {
|
||||
local files=(/etc/ssh/sshd_config)
|
||||
if compgen -G "/etc/ssh/sshd_config.d/*.conf" >/dev/null; then
|
||||
files+=(/etc/ssh/sshd_config.d/*.conf)
|
||||
fi
|
||||
awk '/^[[:space:]]*Port[[:space:]]+/ {print $2}' "${files[@]}" 2>/dev/null | tail -1
|
||||
}
|
||||
|
||||
SSH_PORT=$(sshd_effective_port)
|
||||
SSH_PORT=${SSH_PORT:-22}
|
||||
|
||||
echo -e "${WHITE}${BOLD}LXS Server Hardening${NC}"
|
||||
show_separator
|
||||
echo "The following actions will be applied to this server:"
|
||||
[ $DO_UFW -eq 1 ] && echo " • UFW firewall: default deny incoming, allow SSH on port ${SSH_PORT}"
|
||||
[ $DO_FAIL2BAN -eq 1 ] && echo " • fail2ban: enable sshd jail (bantime 1h, maxretry 5)"
|
||||
[ $DO_UNATTENDED -eq 1 ] && echo " • unattended-upgrades: enable automatic security updates"
|
||||
show_separator
|
||||
|
||||
if [ $ASSUME_YES -ne 1 ]; then
|
||||
read -r -p "Proceed? [y/N] " reply
|
||||
case "$reply" in
|
||||
[yY]|[yY][eE][sS]) ;;
|
||||
*) info "Aborted."; exit 0 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
apt_noninteractive
|
||||
wait_for_apt || exit 1
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# UFW
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
setup_ufw() {
|
||||
info "Installing and configuring UFW..."
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq ufw
|
||||
ufw --force reset >/dev/null
|
||||
ufw default deny incoming
|
||||
ufw default allow outgoing
|
||||
ufw allow "${SSH_PORT}/tcp" comment 'SSH'
|
||||
ufw --force enable
|
||||
ok "UFW enabled (SSH on ${SSH_PORT}/tcp allowed)"
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# fail2ban
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
setup_fail2ban() {
|
||||
info "Installing and configuring fail2ban..."
|
||||
apt-get install -y -qq fail2ban
|
||||
cat > /etc/fail2ban/jail.local <<EOF
|
||||
[DEFAULT]
|
||||
bantime = 1h
|
||||
findtime = 10m
|
||||
maxretry = 5
|
||||
backend = systemd
|
||||
|
||||
[sshd]
|
||||
enabled = true
|
||||
port = ${SSH_PORT}
|
||||
EOF
|
||||
systemctl enable --now fail2ban
|
||||
systemctl restart fail2ban
|
||||
ok "fail2ban enabled (sshd jail active)"
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# unattended-upgrades
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
setup_unattended() {
|
||||
info "Installing and enabling unattended-upgrades..."
|
||||
apt-get install -y -qq unattended-upgrades apt-listchanges
|
||||
cat > /etc/apt/apt.conf.d/20auto-upgrades <<'EOF'
|
||||
APT::Periodic::Update-Package-Lists "1";
|
||||
APT::Periodic::Unattended-Upgrade "1";
|
||||
APT::Periodic::AutocleanInterval "7";
|
||||
EOF
|
||||
systemctl enable --now unattended-upgrades
|
||||
ok "unattended-upgrades enabled (security updates only by default)"
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Run
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
[ $DO_UFW -eq 1 ] && setup_ufw
|
||||
[ $DO_FAIL2BAN -eq 1 ] && setup_fail2ban
|
||||
[ $DO_UNATTENDED -eq 1 ] && setup_unattended
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Summary
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
echo ""
|
||||
echo -e "${WHITE}${BOLD}Summary${NC}"
|
||||
show_separator
|
||||
command -v ufw >/dev/null && echo "UFW: $(ufw status | head -1)"
|
||||
systemctl is-active fail2ban >/dev/null 2>&1 && echo "fail2ban: active" || echo "fail2ban: inactive"
|
||||
systemctl is-active unattended-upgrades >/dev/null 2>&1 && echo "unattended-upgrades: active" || echo "unattended-upgrades: inactive"
|
||||
echo ""
|
||||
ok "Hardening complete."
|
||||
Executable
+94
@@ -0,0 +1,94 @@
|
||||
#!/bin/bash
|
||||
|
||||
# LXS - Tools index
|
||||
# Description: Interactive menu listing the scripts in tools/
|
||||
# Author: LXS
|
||||
# Date: 2025
|
||||
|
||||
# Load LXS common library (colors, separator, run_spinner, loggers)
|
||||
LXS_RAW_BASE="${LXS_RAW_BASE:-https://git.hyko.cx/hykocx/lxs/raw/branch/main}"
|
||||
_lib=$(curl -fsSL "${LXS_RAW_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
|
||||
eval "$_lib"
|
||||
unset _lib
|
||||
|
||||
# Run a sibling tool script. Prefers a file next to this script (installed
|
||||
# layout); falls back to downloading from LXS_RAW_BASE.
|
||||
run_sibling() {
|
||||
local script_path=$1
|
||||
shift
|
||||
local script_name self_dir resolved src="${BASH_SOURCE[0]}"
|
||||
script_name=$(basename "$script_path")
|
||||
|
||||
if [ -n "$src" ]; then
|
||||
resolved=$(readlink -f "$src" 2>/dev/null) \
|
||||
|| resolved=$(realpath "$src" 2>/dev/null) \
|
||||
|| resolved="$src"
|
||||
self_dir=$(dirname "$resolved")
|
||||
fi
|
||||
|
||||
# self_dir points at tools/; the install layout puts siblings here too.
|
||||
if [ -n "$self_dir" ] && [ -f "${self_dir}/${script_name}" ]; then
|
||||
chmod +x "${self_dir}/${script_name}" 2>/dev/null || true
|
||||
"${self_dir}/${script_name}" "$@"
|
||||
return $?
|
||||
fi
|
||||
|
||||
local temp_file exit_code
|
||||
temp_file=$(mktemp "/tmp/lxs.${script_name%.*}.XXXXXX.sh")
|
||||
echo -e "${CYAN}[..] Fetching ${BOLD}${script_name}${NC}${CYAN}...${NC}"
|
||||
if curl -fsSL -H "Cache-Control: no-cache" -o "${temp_file}" "${LXS_RAW_BASE}/${script_path}"; then
|
||||
echo -e "${GREEN}[OK] Payload acquired${NC}"
|
||||
chmod +x "${temp_file}"
|
||||
"${temp_file}" "$@"
|
||||
exit_code=$?
|
||||
rm -f "${temp_file}"
|
||||
return $exit_code
|
||||
else
|
||||
echo -e "${RED}[KO] Failed to download ${script_path}${NC}"
|
||||
rm -f "${temp_file}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
menu_tools() {
|
||||
while true; do
|
||||
clear
|
||||
show_box_top "TOOLS" "SYS_DAEMONS"
|
||||
echo ""
|
||||
show_menu_item "01" "System Infos"
|
||||
show_menu_item "02" "Server Benchmark"
|
||||
show_menu_item "03" "Harden Server"
|
||||
show_menu_item "04" "Change Root Password"
|
||||
show_menu_item "05" "Update Server"
|
||||
show_menu_item "06" "Root SSH Password Login"
|
||||
show_menu_item "07" "Welcome Message (MOTD)"
|
||||
show_menu_item "00" "BACK" "" exit
|
||||
echo ""
|
||||
show_box_bottom
|
||||
echo ""
|
||||
show_prompt
|
||||
read -r choice
|
||||
|
||||
child_rc=0
|
||||
case $choice in
|
||||
1|01) run_sibling "tools/system-infos.sh"; child_rc=$? ;;
|
||||
2|02) run_sibling "tools/server-benchmark.sh"; child_rc=$? ;;
|
||||
3|03) run_sibling "tools/harden.sh"; child_rc=$? ;;
|
||||
4|04) run_sibling "tools/root-password.sh"; child_rc=$? ;;
|
||||
5|05) run_sibling "tools/update-server.sh"; child_rc=$? ;;
|
||||
6|06) run_sibling "tools/root-ssh-login.sh"; child_rc=$? ;;
|
||||
7|07) run_sibling "tools/welcome-message.sh"; child_rc=$? ;;
|
||||
0|00) return ;;
|
||||
*) echo -e "${RED}[KO] Invalid protocol. Select 0-7.${NC}"; sleep 1; continue ;;
|
||||
esac
|
||||
|
||||
# Exit code 75 from a child means it already paused on its own
|
||||
# (its own "Back" or end-of-run prompt) — skip the redundant prompt.
|
||||
if [ "$child_rc" -ne 75 ]; then
|
||||
echo ""
|
||||
read -r -p "Press Enter to continue..."
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
menu_tools
|
||||
Executable
+121
@@ -0,0 +1,121 @@
|
||||
#!/bin/bash
|
||||
|
||||
# LXS - Change root password
|
||||
# Description: Change the root account password (interactive or generated)
|
||||
# Author: LXS
|
||||
# Date: 2025
|
||||
|
||||
# Load LXS common library (colors, separator, run_spinner, loggers, helpers)
|
||||
LXS_RAW_BASE="${LXS_RAW_BASE:-https://git.hyko.cx/hykocx/lxs/raw/branch/main}"
|
||||
_lib=$(curl -fsSL "${LXS_RAW_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
|
||||
eval "$_lib"
|
||||
unset _lib
|
||||
export LXS_LOG_FILE="/tmp/lxs_root_password.log"
|
||||
|
||||
require_root "$0" "$@"
|
||||
|
||||
set -u
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Arguments
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
MODE=""
|
||||
PASSWORD_LENGTH=24
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
-g|--generate) MODE="generate" ;;
|
||||
-i|--interactive) MODE="interactive" ;;
|
||||
--length=*) PASSWORD_LENGTH="${arg#*=}" ;;
|
||||
-h|--help)
|
||||
cat <<EOF
|
||||
Usage: root-password.sh [options]
|
||||
|
||||
Options:
|
||||
-i, --interactive Prompt for the new password (passwd root)
|
||||
-g, --generate Generate a strong random password and apply it
|
||||
--length=N Length of the generated password (default: 24)
|
||||
-h, --help Show this help
|
||||
|
||||
With no option, an interactive menu is shown.
|
||||
EOF
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Unknown option: $arg${NC}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Actions
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
change_interactive() {
|
||||
info "Setting root password interactively..."
|
||||
show_separator
|
||||
if passwd root; then
|
||||
show_separator
|
||||
ok "Root password updated"
|
||||
return 0
|
||||
fi
|
||||
show_separator
|
||||
err "Failed to update root password"
|
||||
return 1
|
||||
}
|
||||
|
||||
change_generated() {
|
||||
if ! [[ "$PASSWORD_LENGTH" =~ ^[0-9]+$ ]] || [ "$PASSWORD_LENGTH" -lt 12 ]; then
|
||||
err "--length must be a number ≥ 12 (got: ${PASSWORD_LENGTH})"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local new_password
|
||||
new_password=$(generate_password "$PASSWORD_LENGTH")
|
||||
if [ -z "$new_password" ]; then
|
||||
err "Failed to generate a password"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! echo "root:${new_password}" | chpasswd; then
|
||||
err "Failed to apply the generated password"
|
||||
return 1
|
||||
fi
|
||||
|
||||
show_separator
|
||||
ok "Root password updated"
|
||||
echo ""
|
||||
echo -e "${WHITE}${BOLD}New root password:${NC} ${YELLOW}${new_password}${NC}"
|
||||
echo ""
|
||||
warn "Store this password in a secure place. It will not be shown again."
|
||||
show_separator
|
||||
return 0
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Menu (when no mode is given on the CLI)
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
if [ -z "$MODE" ]; then
|
||||
echo -e "${WHITE}${BOLD}LXS - Change root password${NC}"
|
||||
show_separator
|
||||
echo -e " ${CYAN}[1]${NC} Set a new password interactively"
|
||||
echo -e " ${CYAN}[2]${NC} Generate a strong random password"
|
||||
echo -e " ${RED}[0]${NC} Cancel"
|
||||
echo ""
|
||||
echo -e -n "${BOLD}Choice [0-2]: ${NC}"
|
||||
read -r choice
|
||||
case "$choice" in
|
||||
1) MODE="interactive" ;;
|
||||
2) MODE="generate" ;;
|
||||
0|"") info "Cancelled."; exit 0 ;;
|
||||
*) err "Invalid option."; exit 1 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
case "$MODE" in
|
||||
interactive) change_interactive ;;
|
||||
generate) change_generated ;;
|
||||
esac
|
||||
Executable
+189
@@ -0,0 +1,189 @@
|
||||
#!/bin/bash
|
||||
|
||||
# LXS - Root SSH password login
|
||||
# Description: Enable or disable root login over SSH with a password
|
||||
# Author: LXS
|
||||
# Date: 2025
|
||||
|
||||
# Load LXS common library (colors, separator, run_spinner, loggers, helpers)
|
||||
LXS_RAW_BASE="${LXS_RAW_BASE:-https://git.hyko.cx/hykocx/lxs/raw/branch/main}"
|
||||
_lib=$(curl -fsSL "${LXS_RAW_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
|
||||
eval "$_lib"
|
||||
unset _lib
|
||||
export LXS_LOG_FILE="/tmp/lxs_root_ssh_login.log"
|
||||
|
||||
require_root "$0" "$@"
|
||||
|
||||
set -u
|
||||
|
||||
# Drop-in path. Numeric prefix `00-` makes it win over distro defaults — sshd
|
||||
# applies the first match per option across the included files.
|
||||
DROPIN_FILE="/etc/ssh/sshd_config.d/00-lxs-root-login.conf"
|
||||
SSH_SERVICE="ssh"
|
||||
command -v systemctl >/dev/null 2>&1 && systemctl list-unit-files 2>/dev/null | grep -q '^sshd\.service' && SSH_SERVICE="sshd"
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Arguments
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
ACTION=""
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--enable|enable) ACTION="enable" ;;
|
||||
--disable|disable) ACTION="disable" ;;
|
||||
--status|status) ACTION="status" ;;
|
||||
-h|--help)
|
||||
cat <<EOF
|
||||
Usage: root-ssh-login.sh [action]
|
||||
|
||||
Actions:
|
||||
--enable Allow root to log in over SSH with a password
|
||||
--disable Disallow root password login (restore SSH defaults)
|
||||
--status Show the current effective settings
|
||||
-h, --help Show this help
|
||||
|
||||
With no action, an interactive menu is shown.
|
||||
|
||||
Note: the change is written to ${DROPIN_FILE}
|
||||
and applied by reloading the SSH service after a successful 'sshd -t'.
|
||||
EOF
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Unknown option: $arg${NC}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Pre-checks
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
if [ ! -d /etc/ssh ] || ! command -v sshd >/dev/null 2>&1; then
|
||||
err "OpenSSH server is not installed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Helpers
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
current_setting() {
|
||||
local key=$1
|
||||
sshd -T 2>/dev/null | awk -v k="${key,,}" 'tolower($1)==k {print $2; exit}'
|
||||
}
|
||||
|
||||
show_status() {
|
||||
local permit pwauth
|
||||
permit=$(current_setting PermitRootLogin)
|
||||
pwauth=$(current_setting PasswordAuthentication)
|
||||
echo -e "${WHITE}${BOLD}Current SSH login settings${NC}"
|
||||
show_separator
|
||||
echo -e " PermitRootLogin: ${BOLD}${permit:-unknown}${NC}"
|
||||
echo -e " PasswordAuthentication: ${BOLD}${pwauth:-unknown}${NC}"
|
||||
if [ -f "$DROPIN_FILE" ]; then
|
||||
echo -e " Drop-in: ${GRAY}${DROPIN_FILE}${NC}"
|
||||
else
|
||||
echo -e " Drop-in: ${GRAY}(none — distro defaults)${NC}"
|
||||
fi
|
||||
show_separator
|
||||
if [ "${permit:-}" = "yes" ] && [ "${pwauth:-}" = "yes" ]; then
|
||||
warn "Root password login is currently ENABLED."
|
||||
else
|
||||
ok "Root password login is currently DISABLED."
|
||||
fi
|
||||
}
|
||||
|
||||
reload_sshd() {
|
||||
if ! sshd -t 2>>"$LXS_LOG_FILE"; then
|
||||
err "sshd config test failed — see ${LXS_LOG_FILE}. Reverting."
|
||||
return 1
|
||||
fi
|
||||
if systemctl reload "$SSH_SERVICE" 2>>"$LXS_LOG_FILE"; then
|
||||
ok "${SSH_SERVICE} reloaded"
|
||||
return 0
|
||||
fi
|
||||
# Fall back to restart (some minimal images ship without reload support)
|
||||
if systemctl restart "$SSH_SERVICE" 2>>"$LXS_LOG_FILE"; then
|
||||
ok "${SSH_SERVICE} restarted"
|
||||
return 0
|
||||
fi
|
||||
err "Failed to reload/restart ${SSH_SERVICE} — see ${LXS_LOG_FILE}"
|
||||
return 1
|
||||
}
|
||||
|
||||
enable_root_login() {
|
||||
# Warn if root has no password — enabling password auth would be useless.
|
||||
local pw_status
|
||||
pw_status=$(passwd -S root 2>/dev/null | awk '{print $2}')
|
||||
case "$pw_status" in
|
||||
L|NP)
|
||||
warn "Root account has no usable password (status: ${pw_status})."
|
||||
warn "Run 'lxs tool root-password' first, otherwise login will still fail."
|
||||
;;
|
||||
esac
|
||||
|
||||
cat > "${DROPIN_FILE}.tmp" <<'EOF'
|
||||
# Managed by LXS (lxs tool root-ssh-login). Remove this file to revert.
|
||||
PermitRootLogin yes
|
||||
PasswordAuthentication yes
|
||||
EOF
|
||||
chmod 644 "${DROPIN_FILE}.tmp"
|
||||
mv "${DROPIN_FILE}.tmp" "$DROPIN_FILE"
|
||||
|
||||
if ! reload_sshd; then
|
||||
rm -f "$DROPIN_FILE"
|
||||
reload_sshd >/dev/null 2>&1 || true
|
||||
return 1
|
||||
fi
|
||||
ok "Root password login over SSH is now ENABLED"
|
||||
warn "This weakens server security. Disable it again when no longer needed:"
|
||||
echo -e " ${GRAY}lxs tool root-ssh-login --disable${NC}"
|
||||
}
|
||||
|
||||
disable_root_login() {
|
||||
if [ ! -f "$DROPIN_FILE" ]; then
|
||||
info "Drop-in not present — root password login already follows SSH defaults."
|
||||
else
|
||||
local backup="${DROPIN_FILE}.bak.$$"
|
||||
mv "$DROPIN_FILE" "$backup"
|
||||
if ! reload_sshd; then
|
||||
mv "$backup" "$DROPIN_FILE"
|
||||
reload_sshd >/dev/null 2>&1 || true
|
||||
return 1
|
||||
fi
|
||||
rm -f "$backup"
|
||||
fi
|
||||
ok "Root password login over SSH is now DISABLED"
|
||||
echo ""
|
||||
show_status
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Menu (when no action is given on the CLI)
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
if [ -z "$ACTION" ]; then
|
||||
show_status
|
||||
echo ""
|
||||
echo -e " ${GREEN}[1]${NC} Enable root SSH password login"
|
||||
echo -e " ${YELLOW}[2]${NC} Disable root SSH password login"
|
||||
echo -e " ${RED}[0]${NC} Cancel"
|
||||
echo ""
|
||||
echo -e -n "${BOLD}Choice [0-2]: ${NC}"
|
||||
read -r choice
|
||||
case "$choice" in
|
||||
1) ACTION="enable" ;;
|
||||
2) ACTION="disable" ;;
|
||||
0|"") info "Cancelled."; exit 0 ;;
|
||||
*) err "Invalid option."; exit 1 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
case "$ACTION" in
|
||||
enable) enable_root_login ;;
|
||||
disable) disable_root_login ;;
|
||||
status) show_status ;;
|
||||
esac
|
||||
Executable
+815
@@ -0,0 +1,815 @@
|
||||
#!/bin/bash
|
||||
|
||||
# LXS - Server Benchmark Tool
|
||||
# Description: Performance testing and benchmarking tool
|
||||
# Author: LXS
|
||||
# Date: 2025
|
||||
|
||||
# Load LXS common library (colors, separator, run_spinner, loggers)
|
||||
LXS_RAW_BASE="${LXS_RAW_BASE:-https://git.hyko.cx/hykocx/lxs/raw/branch/main}"
|
||||
_lib=$(curl -fsSL "${LXS_RAW_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
|
||||
eval "$_lib"
|
||||
unset _lib
|
||||
export LXS_LOG_FILE="/tmp/lxs_benchmark.log"
|
||||
|
||||
# Always clean up benchmark artifacts on exit, even on error or Ctrl+C. Without
|
||||
# this, a partial 1-2GB dd file in /tmp can stay behind and fill the disk on
|
||||
# small servers if the script is re-run.
|
||||
cleanup_benchmark_artifacts() {
|
||||
rm -f /tmp/lxs_test_write_* \
|
||||
/tmp/lxs_test_read_* \
|
||||
/tmp/lxs_write_result_*.tmp \
|
||||
/tmp/lxs_read_result_*.tmp \
|
||||
/tmp/lxs_cpu_result.tmp \
|
||||
/tmp/lxs_ram_result.tmp \
|
||||
/tmp/lxs_ping_result.tmp \
|
||||
/tmp/lxs_ping_*.tmp 2>/dev/null || true
|
||||
}
|
||||
trap cleanup_benchmark_artifacts EXIT INT TERM
|
||||
|
||||
# Minimum free space (MB) to keep available during disk tests. If /tmp drops
|
||||
# below this threshold mid-run we abort the remaining passes.
|
||||
DISK_SAFETY_MARGIN_MB=200
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Score Variables
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
SCORE_CPU=0
|
||||
SCORE_RAM=0
|
||||
SCORE_DISK_WRITE=0
|
||||
SCORE_DISK_READ=0
|
||||
SCORE_NETWORK=0
|
||||
SCORE_NETWORK_LATENCY=0
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Helper Functions
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
show_header() {
|
||||
show_box_top "SERVER PERFORMANCE BENCHMARK"
|
||||
}
|
||||
|
||||
install_dependencies() {
|
||||
local deps_installed=true
|
||||
|
||||
# Check for sysbench
|
||||
if ! command -v sysbench &> /dev/null; then
|
||||
echo -e "${YELLOW}[!] sysbench not found, installing...${NC}"
|
||||
echo ""
|
||||
|
||||
require_disk_space 300 || return 1
|
||||
|
||||
if command -v apt-get &> /dev/null; then
|
||||
run_spinner "Updating package list..." "apt-get update -qq"
|
||||
run_spinner "Installing sysbench..." "DEBIAN_FRONTEND=noninteractive apt-get install -y -qq sysbench"
|
||||
elif command -v yum &> /dev/null; then
|
||||
run_spinner "Installing sysbench..." "yum install -y -q sysbench"
|
||||
elif command -v dnf &> /dev/null; then
|
||||
run_spinner "Installing sysbench..." "dnf install -y -q sysbench"
|
||||
else
|
||||
echo -e "${RED}[✗] Unable to install sysbench automatically${NC}"
|
||||
deps_installed=false
|
||||
fi
|
||||
|
||||
# Verify installation
|
||||
if ! command -v sysbench &> /dev/null; then
|
||||
deps_installed=false
|
||||
fi
|
||||
else
|
||||
echo -e "${GREEN}[✓] All dependencies ready${NC}"
|
||||
fi
|
||||
|
||||
if [ "$deps_installed" = true ]; then
|
||||
echo ""
|
||||
return 0
|
||||
else
|
||||
echo -e "${RED}[✗] Failed to install required dependencies${NC}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# System Information
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
show_system_info() {
|
||||
echo -e "${WHITE}${BOLD}SYSTEM INFORMATION${NC}"
|
||||
show_separator
|
||||
|
||||
local hostname=$(hostname)
|
||||
local cpu_model=$(grep "model name" /proc/cpuinfo | head -1 | cut -d':' -f2 | xargs)
|
||||
local cpu_cores=$(nproc)
|
||||
local total_mem=$(free -h | awk '/^Mem:/ {print $2}')
|
||||
local os_version=$(lsb_release -ds 2>/dev/null || cat /etc/os-release | grep PRETTY_NAME | cut -d'"' -f2)
|
||||
local kernel=$(uname -r)
|
||||
local disk_info=$(df -h / | awk 'NR==2 {print $2}')
|
||||
|
||||
echo -e "${GRAY}Hostname:${NC} $hostname"
|
||||
echo -e "${GRAY}OS:${NC} $os_version"
|
||||
echo -e "${GRAY}Kernel:${NC} $kernel"
|
||||
echo -e "${GRAY}CPU:${NC} $cpu_model"
|
||||
echo -e "${GRAY}vCPU Cores:${NC} $cpu_cores"
|
||||
echo -e "${GRAY}RAM:${NC} $total_mem"
|
||||
echo -e "${GRAY}Disk Size:${NC} $disk_info"
|
||||
echo -e "${GRAY}Date:${NC} $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Benchmark Tests
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
test_cpu() {
|
||||
echo -e "${WHITE}${BOLD}CPU PERFORMANCE TEST${NC}"
|
||||
show_separator
|
||||
|
||||
local cpu_cores=$(nproc)
|
||||
|
||||
# Run sysbench CPU test with spinner
|
||||
local temp_file="/tmp/lxs_cpu_result.tmp"
|
||||
echo -e "${PURPLE}[*] Testing CPU performance (prime number calculation)...${NC}"
|
||||
sysbench cpu --cpu-max-prime=20000 --threads=$cpu_cores run 2>/dev/null > "$temp_file" &
|
||||
local pid=$!
|
||||
|
||||
local spinstr='|/-\'
|
||||
while kill -0 $pid 2>/dev/null; do
|
||||
local temp=${spinstr#?}
|
||||
printf "\r${PURPLE}[%c]${NC} Testing CPU performance (prime number calculation)..." "$spinstr"
|
||||
local spinstr=$temp${spinstr%"$temp"}
|
||||
sleep 0.15
|
||||
done
|
||||
|
||||
wait $pid
|
||||
CPU_RESULT=$(grep "events per second" "$temp_file" | awk '{print $4}')
|
||||
rm -f "$temp_file"
|
||||
|
||||
if [ -z "$CPU_RESULT" ]; then
|
||||
printf "\r${RED}[✗]${NC} CPU test failed \n"
|
||||
SCORE_CPU=0
|
||||
else
|
||||
printf "\r${GREEN}[✓]${NC} CPU test completed \n"
|
||||
SCORE_CPU=$(echo "$CPU_RESULT" | awk '{printf "%.0f", $1}')
|
||||
echo -e "${GREEN} Events per second:${NC} $CPU_RESULT"
|
||||
echo -e "${CYAN} CPU Score:${NC} ${BOLD}$SCORE_CPU points${NC}"
|
||||
fi
|
||||
echo ""
|
||||
}
|
||||
|
||||
test_memory() {
|
||||
echo -e "${WHITE}${BOLD}MEMORY PERFORMANCE TEST${NC}"
|
||||
show_separator
|
||||
|
||||
local cpu_cores=$(nproc)
|
||||
|
||||
# Run sysbench memory test with spinner
|
||||
local temp_file="/tmp/lxs_ram_result.tmp"
|
||||
echo -e "${PURPLE}[*] Testing memory performance (transfer speed)...${NC}"
|
||||
sysbench memory --memory-total-size=5G --threads=$cpu_cores run 2>/dev/null > "$temp_file" &
|
||||
local pid=$!
|
||||
|
||||
local spinstr='|/-\'
|
||||
while kill -0 $pid 2>/dev/null; do
|
||||
local temp=${spinstr#?}
|
||||
printf "\r${PURPLE}[%c]${NC} Testing memory performance (transfer speed)..." "$spinstr"
|
||||
local spinstr=$temp${spinstr%"$temp"}
|
||||
sleep 0.15
|
||||
done
|
||||
|
||||
wait $pid
|
||||
RAM_RESULT=$(grep "transferred" "$temp_file" | awk '{print $(NF-1)}' | sed 's/[^0-9.]//g')
|
||||
rm -f "$temp_file"
|
||||
|
||||
if [ -z "$RAM_RESULT" ]; then
|
||||
printf "\r${RED}[✗]${NC} Memory test failed \n"
|
||||
SCORE_RAM=0
|
||||
else
|
||||
printf "\r${GREEN}[✓]${NC} Memory test completed \n"
|
||||
SCORE_RAM=$(echo "$RAM_RESULT" | awk '{printf "%.0f", $1}')
|
||||
echo -e "${GREEN} Transfer speed:${NC} $RAM_RESULT MiB/sec"
|
||||
echo -e "${CYAN} RAM Score:${NC} ${BOLD}$SCORE_RAM points${NC}"
|
||||
fi
|
||||
echo ""
|
||||
}
|
||||
|
||||
test_disk_write() {
|
||||
echo -e "${WHITE}${BOLD}DISK WRITE PERFORMANCE TEST${NC}"
|
||||
show_separator
|
||||
|
||||
# Check available disk space in /tmp
|
||||
local available_space=$(df -BM /tmp | awk 'NR==2 {print $4}' | sed 's/M//')
|
||||
|
||||
# Build test configurations dynamically based on free space. Each test needs
|
||||
# room for its file PLUS the safety margin, otherwise dd can fill the disk
|
||||
# on small servers (1-2GB VPS). Tests are added from smallest to largest.
|
||||
local test_configs=()
|
||||
if [ "$available_space" -gt $((100 + DISK_SAFETY_MARGIN_MB)) ]; then
|
||||
test_configs+=("100:1M:100MB Sequential")
|
||||
fi
|
||||
if [ "$available_space" -gt $((1024 + DISK_SAFETY_MARGIN_MB)) ]; then
|
||||
test_configs+=("1024:1M:1GB Sequential")
|
||||
fi
|
||||
if [ "$available_space" -gt $((2048 + DISK_SAFETY_MARGIN_MB)) ]; then
|
||||
test_configs+=("2048:1M:2GB Sequential")
|
||||
fi
|
||||
|
||||
if [ ${#test_configs[@]} -eq 0 ]; then
|
||||
echo -e "${RED}[✗] Not enough free space on /tmp (${available_space}MB) to run any write test${NC}"
|
||||
SCORE_DISK_WRITE=0
|
||||
echo ""
|
||||
return 0
|
||||
fi
|
||||
|
||||
local all_speeds=()
|
||||
local all_speeds_mb=()
|
||||
local test_count=0
|
||||
local spinstr='|/-\'
|
||||
|
||||
local test_count_display=$((${#test_configs[@]} * 3))
|
||||
echo -e "${PURPLE}[*] Running advanced write tests (${#test_configs[@]} sizes x 3 passes = ${test_count_display} tests)...${NC}"
|
||||
if [ "$available_space" -le 3072 ]; then
|
||||
echo -e "${YELLOW}[!] Limited disk space (${available_space}MB free), running reduced test set${NC}"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Run tests for each configuration
|
||||
for config in "${test_configs[@]}"; do
|
||||
local size_mb=$(echo "$config" | cut -d':' -f1)
|
||||
local block_size=$(echo "$config" | cut -d':' -f2)
|
||||
local description=$(echo "$config" | cut -d':' -f3)
|
||||
|
||||
local pass_speeds=()
|
||||
local pass_count=0
|
||||
|
||||
# Re-check free space before each config; the previous test may have
|
||||
# left the filesystem tighter than expected (cache, logs, etc.).
|
||||
local current_free=$(df -BM /tmp | awk 'NR==2 {print $4}' | sed 's/M//')
|
||||
if [ "$current_free" -lt $((size_mb + DISK_SAFETY_MARGIN_MB)) ]; then
|
||||
printf "\r${YELLOW}[!]${NC} ${description}: Skipped (only ${current_free}MB free)\n"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Run 3 passes for each configuration
|
||||
for pass in 1 2 3; do
|
||||
# Clean up before test
|
||||
rm -f /tmp/lxs_test_write_${size_mb}
|
||||
sync
|
||||
|
||||
# Clear cache
|
||||
echo 3 > /proc/sys/vm/drop_caches 2>/dev/null
|
||||
sleep 0.5
|
||||
|
||||
local temp_file="/tmp/lxs_write_result_${size_mb}_${pass}.tmp"
|
||||
|
||||
# Run dd test with direct I/O (suppress error messages)
|
||||
(dd if=/dev/zero of=/tmp/lxs_test_write_${size_mb} bs=${block_size} count=${size_mb} oflag=direct 2>"$temp_file" || true) &
|
||||
local pid=$!
|
||||
|
||||
while kill -0 $pid 2>/dev/null; do
|
||||
local temp=${spinstr#?}
|
||||
printf "\r${PURPLE}[%c]${NC} Testing ${description} (Pass ${pass}/3)..." "$spinstr"
|
||||
local spinstr=$temp${spinstr%"$temp"}
|
||||
sleep 0.15
|
||||
done
|
||||
|
||||
wait $pid
|
||||
local exit_code=$?
|
||||
|
||||
# Parse result
|
||||
local speed_value=""
|
||||
local speed_unit=""
|
||||
|
||||
if [ $exit_code -eq 0 ]; then
|
||||
local dd_output=$(cat "$temp_file")
|
||||
speed_value=$(echo "$dd_output" | tail -1 | grep -Eo '[0-9]+\.?[0-9]* [MGK]B/s' | awk '{print $1}')
|
||||
speed_unit=$(echo "$dd_output" | tail -1 | grep -Eo '[0-9]+\.?[0-9]* [MGK]B/s' | awk '{print $2}')
|
||||
fi
|
||||
|
||||
# If direct I/O failed, try with sync
|
||||
if [ -z "$speed_value" ] || [ "$speed_value" = "0" ] || [ $exit_code -ne 0 ]; then
|
||||
rm -f /tmp/lxs_test_write_${size_mb}
|
||||
(dd if=/dev/zero of=/tmp/lxs_test_write_${size_mb} bs=${block_size} count=${size_mb} conv=fdatasync 2>"$temp_file" || true) &
|
||||
pid=$!
|
||||
|
||||
while kill -0 $pid 2>/dev/null; do
|
||||
local temp=${spinstr#?}
|
||||
printf "\r${PURPLE}[%c]${NC} Testing ${description} (Pass ${pass}/3)..." "$spinstr"
|
||||
local spinstr=$temp${spinstr%"$temp"}
|
||||
sleep 0.15
|
||||
done
|
||||
|
||||
wait $pid
|
||||
sync
|
||||
|
||||
local dd_output=$(cat "$temp_file")
|
||||
speed_value=$(echo "$dd_output" | tail -1 | grep -Eo '[0-9]+\.?[0-9]* [MGK]B/s' | awk '{print $1}')
|
||||
speed_unit=$(echo "$dd_output" | tail -1 | grep -Eo '[0-9]+\.?[0-9]* [MGK]B/s' | awk '{print $2}')
|
||||
fi
|
||||
|
||||
rm -f "$temp_file"
|
||||
|
||||
# Convert to MB/s
|
||||
local speed_mb=0
|
||||
if [ ! -z "$speed_value" ] && [ "$speed_value" != "0" ]; then
|
||||
if [ "$speed_unit" = "GB/s" ]; then
|
||||
speed_mb=$(echo "$speed_value" | awk '{printf "%.2f", $1 * 1024}')
|
||||
elif [ "$speed_unit" = "KB/s" ]; then
|
||||
speed_mb=$(echo "$speed_value" | awk '{printf "%.2f", $1 / 1024}')
|
||||
else
|
||||
speed_mb=$(echo "$speed_value" | awk '{printf "%.2f", $1}')
|
||||
fi
|
||||
pass_speeds+=($speed_mb)
|
||||
((pass_count++))
|
||||
fi
|
||||
|
||||
# Clean up test file
|
||||
rm -f /tmp/lxs_test_write_${size_mb}
|
||||
done
|
||||
|
||||
# Calculate average for this configuration
|
||||
if [ $pass_count -gt 0 ]; then
|
||||
local sum=0
|
||||
for speed in "${pass_speeds[@]}"; do
|
||||
sum=$(echo "$sum + $speed" | bc -l)
|
||||
done
|
||||
local avg=$(echo "scale=2; $sum / $pass_count" | bc -l)
|
||||
|
||||
printf "\r${GREEN}[✓]${NC} ${description}: ${GREEN}${avg} MB/s${NC} (avg of ${pass_count} passes)\n"
|
||||
|
||||
all_speeds_mb+=($avg)
|
||||
((test_count++))
|
||||
else
|
||||
printf "\r${YELLOW}[!]${NC} ${description}: Test failed\n"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
|
||||
# Calculate final score
|
||||
if [ $test_count -gt 0 ]; then
|
||||
local total_speed=0
|
||||
for speed in "${all_speeds_mb[@]}"; do
|
||||
total_speed=$(echo "$total_speed + $speed" | bc -l)
|
||||
done
|
||||
local avg_speed=$(echo "scale=2; $total_speed / $test_count" | bc -l)
|
||||
|
||||
SCORE_DISK_WRITE=$(echo "$avg_speed" | awk '{printf "%.0f", $1}')
|
||||
|
||||
echo -e "${CYAN} Average Write Speed:${NC} ${BOLD}${avg_speed} MB/s${NC}"
|
||||
echo -e "${CYAN} Write Score:${NC} ${BOLD}$SCORE_DISK_WRITE points${NC}"
|
||||
|
||||
# Quality assessment
|
||||
if (( $(echo "$avg_speed >= 1000" | bc -l) )); then
|
||||
echo -e "${GREEN} Quality:${NC} Excellent (NVMe SSD)"
|
||||
elif (( $(echo "$avg_speed >= 400" | bc -l) )); then
|
||||
echo -e "${GREEN} Quality:${NC} Very Good (SATA SSD)"
|
||||
elif (( $(echo "$avg_speed >= 100" | bc -l) )); then
|
||||
echo -e "${CYAN} Quality:${NC} Good (Fast HDD/Basic SSD)"
|
||||
else
|
||||
echo -e "${YELLOW} Quality:${NC} Standard (HDD)"
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}[✗] All write tests failed${NC}"
|
||||
SCORE_DISK_WRITE=0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
}
|
||||
|
||||
test_disk_read() {
|
||||
echo -e "${WHITE}${BOLD}DISK READ PERFORMANCE TEST${NC}"
|
||||
show_separator
|
||||
|
||||
# Check available disk space in /tmp
|
||||
local available_space=$(df -BM /tmp | awk 'NR==2 {print $4}' | sed 's/M//')
|
||||
|
||||
# Build test configurations dynamically based on free space (see write test
|
||||
# for rationale). The read test also writes a source file first, so the
|
||||
# space requirement is the same as the write test.
|
||||
local test_configs=()
|
||||
if [ "$available_space" -gt $((100 + DISK_SAFETY_MARGIN_MB)) ]; then
|
||||
test_configs+=("100:1M:100MB Sequential")
|
||||
fi
|
||||
if [ "$available_space" -gt $((1024 + DISK_SAFETY_MARGIN_MB)) ]; then
|
||||
test_configs+=("1024:1M:1GB Sequential")
|
||||
fi
|
||||
if [ "$available_space" -gt $((2048 + DISK_SAFETY_MARGIN_MB)) ]; then
|
||||
test_configs+=("2048:1M:2GB Sequential")
|
||||
fi
|
||||
|
||||
if [ ${#test_configs[@]} -eq 0 ]; then
|
||||
echo -e "${RED}[✗] Not enough free space on /tmp (${available_space}MB) to run any read test${NC}"
|
||||
SCORE_DISK_READ=0
|
||||
echo ""
|
||||
return 0
|
||||
fi
|
||||
|
||||
local all_speeds_mb=()
|
||||
local test_count=0
|
||||
local spinstr='|/-\'
|
||||
|
||||
local test_count_display=$((${#test_configs[@]} * 3))
|
||||
echo -e "${PURPLE}[*] Running advanced read tests (${#test_configs[@]} sizes x 3 passes = ${test_count_display} tests)...${NC}"
|
||||
if [ "$available_space" -le 3072 ]; then
|
||||
echo -e "${YELLOW}[!] Limited disk space (${available_space}MB free), running reduced test set${NC}"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Run tests for each configuration
|
||||
for config in "${test_configs[@]}"; do
|
||||
local size_mb=$(echo "$config" | cut -d':' -f1)
|
||||
local block_size=$(echo "$config" | cut -d':' -f2)
|
||||
local description=$(echo "$config" | cut -d':' -f3)
|
||||
|
||||
# Re-check free space before each config (same rationale as write test).
|
||||
local current_free=$(df -BM /tmp | awk 'NR==2 {print $4}' | sed 's/M//')
|
||||
if [ "$current_free" -lt $((size_mb + DISK_SAFETY_MARGIN_MB)) ]; then
|
||||
printf "\r${YELLOW}[!]${NC} ${description}: Skipped (only ${current_free}MB free)\n"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Create test file if it doesn't exist
|
||||
if [ ! -f /tmp/lxs_test_read_${size_mb} ]; then
|
||||
dd if=/dev/zero of=/tmp/lxs_test_read_${size_mb} bs=${block_size} count=${size_mb} 2>/dev/null || {
|
||||
# If file creation fails, skip this test and remove any partial file
|
||||
rm -f /tmp/lxs_test_read_${size_mb}
|
||||
continue
|
||||
}
|
||||
sync
|
||||
fi
|
||||
|
||||
local pass_speeds=()
|
||||
local pass_count=0
|
||||
|
||||
# Run 3 passes for each configuration
|
||||
for pass in 1 2 3; do
|
||||
# Clear cache before each test
|
||||
sync
|
||||
echo 3 > /proc/sys/vm/drop_caches 2>/dev/null
|
||||
sleep 0.5
|
||||
|
||||
local temp_file="/tmp/lxs_read_result_${size_mb}_${pass}.tmp"
|
||||
|
||||
# Run dd read test with direct I/O (suppress error messages)
|
||||
(dd if=/tmp/lxs_test_read_${size_mb} of=/dev/null bs=${block_size} iflag=direct 2>"$temp_file" || true) &
|
||||
local pid=$!
|
||||
|
||||
while kill -0 $pid 2>/dev/null; do
|
||||
local temp=${spinstr#?}
|
||||
printf "\r${PURPLE}[%c]${NC} Testing ${description} (Pass ${pass}/3)..." "$spinstr"
|
||||
local spinstr=$temp${spinstr%"$temp"}
|
||||
sleep 0.15
|
||||
done
|
||||
|
||||
wait $pid
|
||||
local exit_code=$?
|
||||
|
||||
# Parse result
|
||||
local speed_value=""
|
||||
local speed_unit=""
|
||||
|
||||
if [ $exit_code -eq 0 ]; then
|
||||
local dd_output=$(cat "$temp_file")
|
||||
speed_value=$(echo "$dd_output" | tail -1 | grep -Eo '[0-9]+\.?[0-9]* [MGK]B/s' | awk '{print $1}')
|
||||
speed_unit=$(echo "$dd_output" | tail -1 | grep -Eo '[0-9]+\.?[0-9]* [MGK]B/s' | awk '{print $2}')
|
||||
fi
|
||||
|
||||
# If direct I/O failed, try without it
|
||||
if [ -z "$speed_value" ] || [ "$speed_value" = "0" ] || [ $exit_code -ne 0 ]; then
|
||||
# Clear cache again
|
||||
sync
|
||||
echo 3 > /proc/sys/vm/drop_caches 2>/dev/null
|
||||
|
||||
(dd if=/tmp/lxs_test_read_${size_mb} of=/dev/null bs=${block_size} 2>"$temp_file" || true) &
|
||||
pid=$!
|
||||
|
||||
while kill -0 $pid 2>/dev/null; do
|
||||
local temp=${spinstr#?}
|
||||
printf "\r${PURPLE}[%c]${NC} Testing ${description} (Pass ${pass}/3)..." "$spinstr"
|
||||
local spinstr=$temp${spinstr%"$temp"}
|
||||
sleep 0.15
|
||||
done
|
||||
|
||||
wait $pid
|
||||
|
||||
local dd_output=$(cat "$temp_file")
|
||||
speed_value=$(echo "$dd_output" | tail -1 | grep -Eo '[0-9]+\.?[0-9]* [MGK]B/s' | awk '{print $1}')
|
||||
speed_unit=$(echo "$dd_output" | tail -1 | grep -Eo '[0-9]+\.?[0-9]* [MGK]B/s' | awk '{print $2}')
|
||||
fi
|
||||
|
||||
rm -f "$temp_file"
|
||||
|
||||
# Convert to MB/s
|
||||
local speed_mb=0
|
||||
if [ ! -z "$speed_value" ] && [ "$speed_value" != "0" ]; then
|
||||
if [ "$speed_unit" = "GB/s" ]; then
|
||||
speed_mb=$(echo "$speed_value" | awk '{printf "%.2f", $1 * 1024}')
|
||||
elif [ "$speed_unit" = "KB/s" ]; then
|
||||
speed_mb=$(echo "$speed_value" | awk '{printf "%.2f", $1 / 1024}')
|
||||
else
|
||||
speed_mb=$(echo "$speed_value" | awk '{printf "%.2f", $1}')
|
||||
fi
|
||||
pass_speeds+=($speed_mb)
|
||||
((pass_count++))
|
||||
fi
|
||||
done
|
||||
|
||||
# Calculate average for this configuration
|
||||
if [ $pass_count -gt 0 ]; then
|
||||
local sum=0
|
||||
for speed in "${pass_speeds[@]}"; do
|
||||
sum=$(echo "$sum + $speed" | bc -l)
|
||||
done
|
||||
local avg=$(echo "scale=2; $sum / $pass_count" | bc -l)
|
||||
|
||||
printf "\r${GREEN}[✓]${NC} ${description}: ${GREEN}${avg} MB/s${NC} (avg of ${pass_count} passes)\n"
|
||||
|
||||
all_speeds_mb+=($avg)
|
||||
((test_count++))
|
||||
else
|
||||
printf "\r${YELLOW}[!]${NC} ${description}: Test failed\n"
|
||||
fi
|
||||
|
||||
# Clean up test file
|
||||
rm -f /tmp/lxs_test_read_${size_mb}
|
||||
done
|
||||
|
||||
echo ""
|
||||
|
||||
# Calculate final score
|
||||
if [ $test_count -gt 0 ]; then
|
||||
local total_speed=0
|
||||
for speed in "${all_speeds_mb[@]}"; do
|
||||
total_speed=$(echo "$total_speed + $speed" | bc -l)
|
||||
done
|
||||
local avg_speed=$(echo "scale=2; $total_speed / $test_count" | bc -l)
|
||||
|
||||
SCORE_DISK_READ=$(echo "$avg_speed" | awk '{printf "%.0f", $1}')
|
||||
|
||||
echo -e "${CYAN} Average Read Speed:${NC} ${BOLD}${avg_speed} MB/s${NC}"
|
||||
echo -e "${CYAN} Read Score:${NC} ${BOLD}$SCORE_DISK_READ points${NC}"
|
||||
|
||||
# Quality assessment
|
||||
if (( $(echo "$avg_speed >= 2000" | bc -l) )); then
|
||||
echo -e "${GREEN} Quality:${NC} Excellent (High-end NVMe SSD)"
|
||||
elif (( $(echo "$avg_speed >= 500" | bc -l) )); then
|
||||
echo -e "${GREEN} Quality:${NC} Very Good (NVMe/Fast SATA SSD)"
|
||||
elif (( $(echo "$avg_speed >= 150" | bc -l) )); then
|
||||
echo -e "${CYAN} Quality:${NC} Good (SATA SSD)"
|
||||
else
|
||||
echo -e "${YELLOW} Quality:${NC} Standard (HDD)"
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}[✗] All read tests failed${NC}"
|
||||
SCORE_DISK_READ=0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
}
|
||||
|
||||
test_network() {
|
||||
echo -e "${WHITE}${BOLD}NETWORK PERFORMANCE TEST${NC}"
|
||||
show_separator
|
||||
|
||||
# Test ping to common servers with spinner
|
||||
local temp_file="/tmp/lxs_ping_result.tmp"
|
||||
echo -e "${PURPLE}[*] Testing network latency and speed...${NC}"
|
||||
ping -c 5 8.8.8.8 2>/dev/null > "$temp_file" &
|
||||
local pid=$!
|
||||
|
||||
local spinstr='|/-\'
|
||||
while kill -0 $pid 2>/dev/null; do
|
||||
local temp=${spinstr#?}
|
||||
printf "\r${PURPLE}[%c]${NC} Testing network latency and speed..." "$spinstr"
|
||||
local spinstr=$temp${spinstr%"$temp"}
|
||||
sleep 0.15
|
||||
done
|
||||
|
||||
wait $pid
|
||||
local ping_test=$(tail -1 "$temp_file" | awk -F '/' '{print $5}')
|
||||
rm -f "$temp_file"
|
||||
|
||||
if [ -z "$ping_test" ]; then
|
||||
printf "\r${YELLOW}[!]${NC} Network test skipped (no connectivity) \n"
|
||||
SCORE_NETWORK=0
|
||||
else
|
||||
printf "\r${GREEN}[✓]${NC} Network test completed \n"
|
||||
# Convert latency to score (lower is better, so invert)
|
||||
# Score = 1000 / latency (max 100 points for <10ms)
|
||||
SCORE_NETWORK=$(echo "$ping_test" | awk '{score = 1000 / $1; if (score > 100) score = 100; printf "%.0f", score}')
|
||||
echo -e "${GREEN} Average latency to 8.8.8.8:${NC} $ping_test ms"
|
||||
echo -e "${CYAN} Network Score:${NC} ${BOLD}$SCORE_NETWORK points${NC}"
|
||||
fi
|
||||
echo ""
|
||||
}
|
||||
|
||||
test_network_latency() {
|
||||
echo -e "${WHITE}${BOLD}GEOGRAPHIC NETWORK LATENCY TEST${NC}"
|
||||
show_separator
|
||||
|
||||
# Define test locations with multiple servers for redundancy
|
||||
# Format: "City|Server1,Server2,Server3"
|
||||
local test_locations=(
|
||||
"Montreal|ec2.ca-central-1.amazonaws.com,objectstorage.ca-montreal-1.oraclecloud.com"
|
||||
"Toronto|objectstorage.ca-toronto-1.oraclecloud.com,tor-ca-ping.vultr.com,speedtest-tor1.digitalocean.com,speedtest.toronto1.linode.com"
|
||||
"Vancouver|ec2.us-west-2.amazonaws.com"
|
||||
"New York|ec2.us-east-1.amazonaws.com,speedtest-nyc1.digitalocean.com"
|
||||
"Texas|tx-us-ping.vultr.com"
|
||||
)
|
||||
|
||||
local total_latency=0
|
||||
local successful_tests=0
|
||||
local failed_count=0
|
||||
|
||||
echo -e "${PURPLE}[*] Testing latency to multiple geographic locations...${NC}"
|
||||
echo ""
|
||||
|
||||
# Test each location
|
||||
for location in "${test_locations[@]}"; do
|
||||
local city=$(echo "$location" | cut -d'|' -f1)
|
||||
local servers=$(echo "$location" | cut -d'|' -f2)
|
||||
|
||||
local city_latency=""
|
||||
local city_success=false
|
||||
|
||||
# Try each server for the city until one succeeds
|
||||
IFS=',' read -ra SERVER_ARRAY <<< "$servers"
|
||||
for server in "${SERVER_ARRAY[@]}"; do
|
||||
local temp_file="/tmp/lxs_ping_${city}_${server}.tmp"
|
||||
|
||||
# Ping with timeout (3 pings for faster results)
|
||||
timeout 8 ping -c 3 -W 2 "$server" 2>/dev/null > "$temp_file" &
|
||||
local pid=$!
|
||||
|
||||
local spinstr='|/-\'
|
||||
while kill -0 $pid 2>/dev/null; do
|
||||
local temp=${spinstr#?}
|
||||
printf "\r${PURPLE}[%c]${NC} Testing ${city}..." "$spinstr"
|
||||
local spinstr=$temp${spinstr%"$temp"}
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
wait $pid
|
||||
local exit_code=$?
|
||||
|
||||
# Parse result
|
||||
local avg_latency=$(tail -1 "$temp_file" 2>/dev/null | awk -F '/' '{print $5}')
|
||||
rm -f "$temp_file"
|
||||
|
||||
# Check if we got a valid result
|
||||
if [ ! -z "$avg_latency" ] && [ "$avg_latency" != "0" ] && [ $exit_code -eq 0 ]; then
|
||||
city_latency="$avg_latency"
|
||||
city_success=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Display result for this city
|
||||
if [ "$city_success" = true ]; then
|
||||
printf "\r${GREEN}[✓]${NC} %-12s ${GREEN}%6s ms${NC}\n" "$city:" "$city_latency"
|
||||
total_latency=$(echo "$total_latency + $city_latency" | bc -l 2>/dev/null || echo "$total_latency")
|
||||
((successful_tests++))
|
||||
else
|
||||
printf "\r${YELLOW}[!]${NC} %-12s ${YELLOW}Unreachable${NC}\n" "$city:"
|
||||
((failed_count++))
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
|
||||
# Calculate score
|
||||
if [ $successful_tests -gt 0 ]; then
|
||||
local avg_total=$(echo "scale=2; $total_latency / $successful_tests" | bc -l)
|
||||
|
||||
# Score calculation: Lower latency = higher score
|
||||
# Perfect score (100) for avg latency <= 20ms
|
||||
# Score decreases as latency increases
|
||||
# Formula: max(0, 100 - ((avg_latency - 20) * 0.5))
|
||||
SCORE_NETWORK_LATENCY=$(echo "$avg_total" | awk '{
|
||||
if ($1 <= 20) score = 100;
|
||||
else if ($1 >= 220) score = 0;
|
||||
else score = 100 - (($1 - 20) * 0.5);
|
||||
printf "%.0f", score
|
||||
}')
|
||||
|
||||
local total_locations=$((successful_tests + failed_count))
|
||||
echo -e "${CYAN} Average Latency:${NC} ${avg_total} ms (${successful_tests}/${total_locations} locations)"
|
||||
echo -e "${CYAN} Latency Score:${NC} ${BOLD}$SCORE_NETWORK_LATENCY points${NC}"
|
||||
|
||||
# Provide context on latency quality
|
||||
if (( $(echo "$avg_total <= 50" | bc -l) )); then
|
||||
echo -e "${GREEN} Quality:${NC} Excellent latency across regions"
|
||||
elif (( $(echo "$avg_total <= 100" | bc -l) )); then
|
||||
echo -e "${CYAN} Quality:${NC} Good latency across regions"
|
||||
elif (( $(echo "$avg_total <= 150" | bc -l) )); then
|
||||
echo -e "${YELLOW} Quality:${NC} Average latency across regions"
|
||||
else
|
||||
echo -e "${RED} Quality:${NC} High latency detected"
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}[✗] All latency tests failed${NC}"
|
||||
SCORE_NETWORK_LATENCY=0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Results Display
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
show_results() {
|
||||
show_separator
|
||||
echo -e "${WHITE}${BOLD}BENCHMARK RESULTS${NC}"
|
||||
show_separator
|
||||
echo ""
|
||||
|
||||
echo -e "${GRAY}CPU Score:${NC} ${BOLD}$SCORE_CPU${NC} points"
|
||||
echo -e "${GRAY}RAM Score:${NC} ${BOLD}$SCORE_RAM${NC} points"
|
||||
echo -e "${GRAY}Disk Write Score:${NC} ${BOLD}$SCORE_DISK_WRITE${NC} points"
|
||||
echo -e "${GRAY}Disk Read Score:${NC} ${BOLD}$SCORE_DISK_READ${NC} points"
|
||||
echo -e "${GRAY}Network Score:${NC} ${BOLD}$SCORE_NETWORK${NC} points"
|
||||
echo -e "${GRAY}Network Latency Score:${NC} ${BOLD}$SCORE_NETWORK_LATENCY${NC} points"
|
||||
echo ""
|
||||
show_separator
|
||||
|
||||
# Calculate final score with weighted average
|
||||
# CPU = 30%, RAM = 20%, Disk Write = 15%, Disk Read = 15%, Network = 10%, Latency = 10%
|
||||
SCORE_FINAL=$(awk "BEGIN {printf \"%.0f\", ($SCORE_CPU * 0.30) + ($SCORE_RAM * 0.20) + ($SCORE_DISK_WRITE * 0.15) + ($SCORE_DISK_READ * 0.15) + ($SCORE_NETWORK * 0.10) + ($SCORE_NETWORK_LATENCY * 0.10)}")
|
||||
|
||||
echo -e "${WHITE}${BOLD}FINAL BENCHMARK SCORE${NC}"
|
||||
show_separator
|
||||
echo ""
|
||||
echo -e "${GREEN}${BOLD}$SCORE_FINAL points${NC}"
|
||||
echo ""
|
||||
show_separator
|
||||
echo ""
|
||||
|
||||
# Performance rating
|
||||
if [ "$SCORE_FINAL" -ge 5000 ]; then
|
||||
echo -e "${GREEN}${BOLD}Performance Rating: [★★★★★] Exceptional${NC}"
|
||||
elif [ "$SCORE_FINAL" -ge 3000 ]; then
|
||||
echo -e "${GREEN}${BOLD}Performance Rating: [★★★★☆] Excellent${NC}"
|
||||
elif [ "$SCORE_FINAL" -ge 2000 ]; then
|
||||
echo -e "${CYAN}${BOLD}Performance Rating: [★★★☆☆] Good${NC}"
|
||||
elif [ "$SCORE_FINAL" -ge 1000 ]; then
|
||||
echo -e "${YELLOW}${BOLD}Performance Rating: [★★☆☆☆] Average${NC}"
|
||||
else
|
||||
echo -e "${RED}${BOLD}Performance Rating: [★☆☆☆☆] Below Average${NC}"
|
||||
fi
|
||||
echo ""
|
||||
echo -e "${GRAY}Higher scores indicate better performance.${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Main Execution
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
main() {
|
||||
clear
|
||||
show_header
|
||||
echo ""
|
||||
|
||||
# Show system information
|
||||
show_system_info
|
||||
|
||||
# Install dependencies
|
||||
if ! install_dependencies; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${YELLOW}${BOLD}[!] Starting benchmark tests...${NC}"
|
||||
echo -e "${YELLOW} This may take a few minutes.${NC}"
|
||||
echo ""
|
||||
|
||||
# Run all tests
|
||||
test_cpu
|
||||
test_memory
|
||||
test_disk_write
|
||||
test_disk_read
|
||||
test_network
|
||||
test_network_latency
|
||||
|
||||
# Display results
|
||||
show_results
|
||||
|
||||
# Wait for user input before exiting
|
||||
echo ""
|
||||
read -p "Press Enter to return to menu..."
|
||||
}
|
||||
|
||||
# Check if running as root
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo ""
|
||||
echo -e "${RED}${BOLD}[✗] ERROR: This script must be run as root${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Please run with: sudo $0${NC}"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Execute main function
|
||||
main
|
||||
exit 75
|
||||
|
||||
Executable
+285
@@ -0,0 +1,285 @@
|
||||
#!/bin/bash
|
||||
|
||||
# LXS - System Infos
|
||||
# Description: Essential system monitoring and diagnostic tools
|
||||
# Author: LXS
|
||||
# Date: 2025
|
||||
|
||||
# Load LXS common library (colors, separator, run_spinner, loggers)
|
||||
LXS_RAW_BASE="${LXS_RAW_BASE:-https://git.hyko.cx/hykocx/lxs/raw/branch/main}"
|
||||
_lib=$(curl -fsSL "${LXS_RAW_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
|
||||
eval "$_lib"
|
||||
unset _lib
|
||||
export LXS_LOG_FILE="/tmp/lxs_system_infos.log"
|
||||
|
||||
# Menu: System Infos
|
||||
menu_system_infos() {
|
||||
while true; do
|
||||
clear
|
||||
show_box_top "SYSTEM INFOS"
|
||||
echo ""
|
||||
show_menu_item "1" "View system informations"
|
||||
show_menu_item "2" "Check disk space"
|
||||
show_menu_item "3" "Check memory usage"
|
||||
show_menu_item "4" "Check CPU load"
|
||||
show_menu_item "5" "Check network"
|
||||
show_menu_item "6" "View system logs (last 50 lines)"
|
||||
show_menu_item "7" "Show top resource-consuming processes"
|
||||
show_menu_item "8" "Check disk health (SMART)"
|
||||
show_menu_item "0" "Exit" "" exit
|
||||
echo ""
|
||||
show_box_bottom
|
||||
echo ""
|
||||
show_prompt
|
||||
read -r choice
|
||||
|
||||
echo ""
|
||||
|
||||
case $choice in
|
||||
1)
|
||||
# View system informations
|
||||
clear
|
||||
show_box_top "SYSTEM INFORMATION"
|
||||
echo ""
|
||||
|
||||
echo -e "${CYAN}${BOLD}Operating System:${NC}"
|
||||
echo -e " $(cat /etc/os-release | grep PRETTY_NAME | cut -d'"' -f2)"
|
||||
echo ""
|
||||
|
||||
echo -e "${CYAN}${BOLD}Kernel Version:${NC}"
|
||||
echo -e " $(uname -r)"
|
||||
echo ""
|
||||
|
||||
echo -e "${CYAN}${BOLD}System Uptime:${NC}"
|
||||
echo -e " $(uptime -p 2>/dev/null || uptime | awk -F'up ' '{print $2}' | awk -F',' '{print $1}')"
|
||||
echo ""
|
||||
|
||||
echo -e "${CYAN}${BOLD}Load Average:${NC}"
|
||||
echo -e " $(uptime | awk -F'load average:' '{print $2}')"
|
||||
echo ""
|
||||
|
||||
echo -e "${CYAN}${BOLD}CPU Information:${NC}"
|
||||
echo -e " Model: $(lscpu | grep "Model name" | cut -d':' -f2 | xargs)"
|
||||
echo -e " Cores: $(nproc)"
|
||||
echo -e " Architecture: $(uname -m)"
|
||||
echo ""
|
||||
|
||||
echo -e "${CYAN}${BOLD}Memory:${NC}"
|
||||
local total_mem=$(free -h | awk '/^Mem:/ {print $2}')
|
||||
local used_mem=$(free -h | awk '/^Mem:/ {print $3}')
|
||||
local available_mem=$(free -h | awk '/^Mem:/ {print $7}')
|
||||
echo -e " Total: $total_mem"
|
||||
echo -e " Used: $used_mem"
|
||||
echo -e " Available: $available_mem"
|
||||
echo ""
|
||||
|
||||
echo -e "${CYAN}${BOLD}Disk Space:${NC}"
|
||||
echo -e " Total: $(df -h / | awk 'NR==2 {print $2}')"
|
||||
echo -e " Used: $(df -h / | awk 'NR==2 {print $3}')"
|
||||
echo -e " Available: $(df -h / | awk 'NR==2 {print $4}')"
|
||||
echo -e " Usage: $(df -h / | awk 'NR==2 {print $5}')"
|
||||
echo ""
|
||||
|
||||
echo -e "${CYAN}${BOLD}Network:${NC}"
|
||||
echo -e " Hostname: $(hostname)"
|
||||
echo -e " Local IP: $(hostname -I | awk '{print $1}')"
|
||||
echo -e " Public IP: $(get_public_ip)"
|
||||
echo ""
|
||||
|
||||
echo -e "${CYAN}${BOLD}Installed Software:${NC}"
|
||||
if command -v git &> /dev/null; then
|
||||
echo -e " ${GREEN}[✓]${NC} Git: $(git --version 2>/dev/null | cut -d' ' -f3)"
|
||||
else
|
||||
echo -e " ${GRAY}[ ]${NC} Git: Not installed"
|
||||
fi
|
||||
|
||||
if command -v docker &> /dev/null; then
|
||||
echo -e " ${GREEN}[✓]${NC} Docker: $(docker --version 2>/dev/null | cut -d' ' -f3 | tr -d ',')"
|
||||
else
|
||||
echo -e " ${GRAY}[ ]${NC} Docker: Not installed"
|
||||
fi
|
||||
|
||||
if command -v node &> /dev/null; then
|
||||
echo -e " ${GREEN}[✓]${NC} Node.js: $(node --version 2>/dev/null)"
|
||||
else
|
||||
echo -e " ${GRAY}[ ]${NC} Node.js: Not installed"
|
||||
fi
|
||||
|
||||
if command -v python3 &> /dev/null; then
|
||||
echo -e " ${GREEN}[✓]${NC} Python3: $(python3 --version 2>/dev/null | cut -d' ' -f2)"
|
||||
else
|
||||
echo -e " ${GRAY}[ ]${NC} Python3: Not installed"
|
||||
fi
|
||||
;;
|
||||
2)
|
||||
# Check disk space
|
||||
echo -e "${CYAN}${BOLD}Disk Space Usage:${NC}"
|
||||
echo ""
|
||||
df -h
|
||||
echo ""
|
||||
show_separator
|
||||
echo -e "${CYAN}${BOLD}Inode Usage:${NC}"
|
||||
echo ""
|
||||
df -i
|
||||
;;
|
||||
3)
|
||||
# Check memory usage
|
||||
echo -e "${CYAN}${BOLD}Memory Usage:${NC}"
|
||||
echo ""
|
||||
free -h
|
||||
echo ""
|
||||
show_separator
|
||||
echo -e "${CYAN}${BOLD}Detailed Memory Info:${NC}"
|
||||
echo ""
|
||||
cat /proc/meminfo | head -10
|
||||
;;
|
||||
4)
|
||||
# Check CPU load
|
||||
echo -e "${CYAN}${BOLD}CPU Load and Top Processes:${NC}"
|
||||
echo ""
|
||||
uptime
|
||||
echo ""
|
||||
show_separator
|
||||
echo ""
|
||||
top -bn1 | head -20
|
||||
;;
|
||||
5)
|
||||
# Check network
|
||||
echo -e "${CYAN}${BOLD}Network Check:${NC}"
|
||||
echo ""
|
||||
|
||||
echo -e "${CYAN}[1/5]${NC} Network Interfaces:"
|
||||
echo ""
|
||||
ip -brief addr show 2>/dev/null || ifconfig -a
|
||||
echo ""
|
||||
|
||||
echo -e "${CYAN}[2/5]${NC} Public IP Address:"
|
||||
PUBLIC_IP=$(get_public_ip)
|
||||
echo -e " ${WHITE}$PUBLIC_IP${NC}"
|
||||
echo ""
|
||||
|
||||
echo -e "${CYAN}[3/5]${NC} DNS Resolution Test:"
|
||||
echo -e -n " Testing google.com... "
|
||||
if nslookup google.com >/dev/null 2>&1; then
|
||||
echo -e "${GREEN}OK${NC}"
|
||||
else
|
||||
echo -e "${RED}FAILED${NC}"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
echo -e "${CYAN}[4/5]${NC} Internet Connectivity Test:"
|
||||
echo -e -n " Pinging google.com... "
|
||||
if ping -c 1 google.com >/dev/null 2>&1; then
|
||||
echo -e "${GREEN}OK${NC}"
|
||||
else
|
||||
echo -e "${RED}FAILED${NC}"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
echo -e "${CYAN}[5/5]${NC} Open Ports:"
|
||||
echo ""
|
||||
if command -v ss &> /dev/null; then
|
||||
ss -tulpn 2>/dev/null | grep LISTEN || echo " No listening ports or insufficient permissions"
|
||||
elif command -v netstat &> /dev/null; then
|
||||
netstat -tulpn 2>/dev/null | grep LISTEN || echo " No listening ports or insufficient permissions"
|
||||
else
|
||||
echo " ${YELLOW}Neither ss nor netstat available${NC}"
|
||||
fi
|
||||
;;
|
||||
6)
|
||||
# View system logs
|
||||
echo -e "${CYAN}${BOLD}System Logs (last 50 lines):${NC}"
|
||||
echo ""
|
||||
sudo journalctl -n 50 --no-pager
|
||||
;;
|
||||
7)
|
||||
# Show top resource-consuming processes
|
||||
echo -e "${CYAN}${BOLD}Top Memory-Consuming Processes:${NC}"
|
||||
echo ""
|
||||
ps aux --sort=-%mem | head -15
|
||||
echo ""
|
||||
show_separator
|
||||
echo ""
|
||||
echo -e "${CYAN}${BOLD}Top CPU-Consuming Processes:${NC}"
|
||||
echo ""
|
||||
ps aux --sort=-%cpu | head -15
|
||||
;;
|
||||
8)
|
||||
# Check disk health
|
||||
echo -e "${CYAN}${BOLD}Disk Health Check (SMART):${NC}"
|
||||
echo ""
|
||||
if command -v smartctl &> /dev/null; then
|
||||
disks=$(lsblk -d -n -p -o NAME,TYPE | grep "disk" | awk '{print $1}')
|
||||
if [ -z "$disks" ]; then
|
||||
echo -e "${RED}[✗] No disks found${NC}"
|
||||
else
|
||||
for disk in $disks; do
|
||||
disk_name=$(basename "$disk")
|
||||
show_separator
|
||||
echo -e "${WHITE}Disk: $disk${NC}"
|
||||
|
||||
# Get disk info
|
||||
disk_size=$(lsblk -d -n -o SIZE "$disk" 2>/dev/null)
|
||||
disk_model=$(lsblk -d -n -o MODEL "$disk" 2>/dev/null | xargs)
|
||||
disk_rota=$(cat /sys/block/$disk_name/queue/rotational 2>/dev/null)
|
||||
|
||||
echo -e " Size: ${CYAN}$disk_size${NC}"
|
||||
[ -n "$disk_model" ] && echo -e " Model: ${CYAN}$disk_model${NC}"
|
||||
[ "$disk_rota" == "0" ] && echo -e " Type: ${CYAN}SSD/Virtual${NC}" || echo -e " Type: ${CYAN}HDD${NC}"
|
||||
echo ""
|
||||
|
||||
# Check if it's a virtual disk
|
||||
if [[ $disk == *"/dev/vd"* ]] || [[ $disk == *"/dev/xvd"* ]]; then
|
||||
echo -e " ${YELLOW}[!] Virtual disk detected - SMART not available${NC}"
|
||||
echo -e " ${GRAY}[i] Virtual disks don't support SMART monitoring${NC}"
|
||||
else
|
||||
# Try SMART check for physical disks
|
||||
smart_output=$(sudo smartctl -H "$disk" 2>&1)
|
||||
if echo "$smart_output" | grep -q "PASSED"; then
|
||||
echo -e " ${GREEN}[✓] SMART Status: PASSED${NC}"
|
||||
elif echo "$smart_output" | grep -q "FAILED"; then
|
||||
echo -e " ${RED}[✗] SMART Status: FAILED${NC}"
|
||||
echo -e " ${RED}[!] WARNING: Disk may be failing!${NC}"
|
||||
else
|
||||
echo -e " ${YELLOW}[!] SMART not available for this disk${NC}"
|
||||
fi
|
||||
fi
|
||||
echo ""
|
||||
done
|
||||
show_separator
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}[✗] smartmontools is not installed${NC}"
|
||||
echo ""
|
||||
read -p "Would you like to install it? (y/n): " install_smart
|
||||
if [[ $install_smart =~ ^[Yy]$ ]]; then
|
||||
echo ""
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
export NEEDRESTART_MODE=a
|
||||
export NEEDRESTART_SUSPEND=1
|
||||
run_spinner "Updating package list..." "sudo apt update -qq"
|
||||
run_spinner "Installing smartmontools..." "sudo apt install -y -qq smartmontools -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold'"
|
||||
echo ""
|
||||
echo -e "${GRAY}[i] Run this option again to check disk health${NC}"
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
0)
|
||||
exit 75
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}[✗] Invalid option. Please select 0-8.${NC}"
|
||||
sleep 2
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
show_separator
|
||||
echo ""
|
||||
read -p "Press Enter to continue..."
|
||||
done
|
||||
}
|
||||
|
||||
menu_system_infos
|
||||
|
||||
Executable
+150
@@ -0,0 +1,150 @@
|
||||
#!/bin/bash
|
||||
|
||||
# LXS - Update server
|
||||
# Description: Refresh package lists and upgrade installed packages
|
||||
# Author: LXS
|
||||
# Date: 2025
|
||||
|
||||
# Load LXS common library (colors, separator, run_spinner, loggers, helpers)
|
||||
LXS_RAW_BASE="${LXS_RAW_BASE:-https://git.hyko.cx/hykocx/lxs/raw/branch/main}"
|
||||
_lib=$(curl -fsSL "${LXS_RAW_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
|
||||
eval "$_lib"
|
||||
unset _lib
|
||||
export LXS_LOG_FILE="/tmp/lxs_update_server.log"
|
||||
|
||||
require_root "$0" "$@"
|
||||
|
||||
set -u
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Arguments
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
DO_FULL_UPGRADE=0
|
||||
DO_AUTOREMOVE=1
|
||||
DO_AUTOCLEAN=1
|
||||
ASSUME_YES=0
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--full|--dist-upgrade) DO_FULL_UPGRADE=1 ;;
|
||||
--no-autoremove) DO_AUTOREMOVE=0 ;;
|
||||
--no-autoclean) DO_AUTOCLEAN=0 ;;
|
||||
-y|--yes) ASSUME_YES=1 ;;
|
||||
-h|--help)
|
||||
cat <<EOF
|
||||
Usage: update-server.sh [options]
|
||||
|
||||
Options:
|
||||
--full, --dist-upgrade Use 'apt-get full-upgrade' (may add/remove packages)
|
||||
--no-autoremove Skip 'apt-get autoremove'
|
||||
--no-autoclean Skip 'apt-get autoclean'
|
||||
-y, --yes Skip confirmation prompt
|
||||
-h, --help Show this help
|
||||
EOF
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Unknown option: $arg${NC}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Pre-checks
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
require_debian_ubuntu || exit 1
|
||||
|
||||
if ! command -v apt-get >/dev/null 2>&1; then
|
||||
err "apt-get is not available on this system"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
require_disk_space 1024 || exit 1
|
||||
|
||||
UPGRADE_CMD="upgrade"
|
||||
UPGRADE_LABEL="Upgrade installed packages"
|
||||
if [ $DO_FULL_UPGRADE -eq 1 ]; then
|
||||
UPGRADE_CMD="full-upgrade"
|
||||
UPGRADE_LABEL="Full-upgrade (may add/remove packages)"
|
||||
fi
|
||||
|
||||
echo -e "${WHITE}${BOLD}LXS Server Update${NC}"
|
||||
show_separator
|
||||
echo "The following actions will be performed:"
|
||||
echo " • Refresh package lists (apt-get update)"
|
||||
echo " • ${UPGRADE_LABEL}"
|
||||
[ $DO_AUTOREMOVE -eq 1 ] && echo " • Remove unused packages (apt-get autoremove)"
|
||||
[ $DO_AUTOCLEAN -eq 1 ] && echo " • Clean old package archives (apt-get autoclean)"
|
||||
show_separator
|
||||
|
||||
if [ $ASSUME_YES -ne 1 ]; then
|
||||
echo -e -n "${BOLD}Proceed? [y/N]: ${NC}"
|
||||
read -r reply
|
||||
case "$reply" in
|
||||
[yY]|[yY][eE][sS]) ;;
|
||||
*) info "Cancelled."; exit 0 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Run
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
apt_noninteractive
|
||||
wait_for_apt || exit 1
|
||||
|
||||
APT_OPTS='-y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold"'
|
||||
|
||||
run_spinner "Refreshing package lists..." "apt-get update" || {
|
||||
err "apt-get update failed — see ${LXS_LOG_FILE}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
run_spinner "${UPGRADE_LABEL}..." "apt-get ${APT_OPTS} ${UPGRADE_CMD}" || {
|
||||
err "apt-get ${UPGRADE_CMD} failed — see ${LXS_LOG_FILE}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [ $DO_AUTOREMOVE -eq 1 ]; then
|
||||
run_spinner "Removing unused packages..." "apt-get ${APT_OPTS} autoremove --purge" \
|
||||
|| warn "autoremove failed — see ${LXS_LOG_FILE}"
|
||||
fi
|
||||
|
||||
if [ $DO_AUTOCLEAN -eq 1 ]; then
|
||||
run_spinner "Cleaning old archives..." "apt-get ${APT_OPTS} autoclean" \
|
||||
|| warn "autoclean failed — see ${LXS_LOG_FILE}"
|
||||
fi
|
||||
|
||||
show_separator
|
||||
ok "Server updated"
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Reboot hint
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
if [ -f /var/run/reboot-required ]; then
|
||||
echo ""
|
||||
warn "A reboot is required to complete the update."
|
||||
if [ -f /var/run/reboot-required.pkgs ]; then
|
||||
echo -e "${GRAY}Packages requiring reboot:${NC}"
|
||||
sed 's/^/ • /' /var/run/reboot-required.pkgs
|
||||
fi
|
||||
|
||||
if [ -t 0 ]; then
|
||||
echo ""
|
||||
echo -e -n "${BOLD}Reboot now? [y/N]: ${NC}"
|
||||
read -r reboot_reply
|
||||
case "$reboot_reply" in
|
||||
[yY]|[yY][eE][sS])
|
||||
info "Rebooting..."
|
||||
systemctl reboot
|
||||
;;
|
||||
*)
|
||||
info "Reboot skipped. Remember to reboot later."
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
fi
|
||||
Executable
+203
@@ -0,0 +1,203 @@
|
||||
#!/bin/bash
|
||||
|
||||
# LXS - Welcome message (MOTD)
|
||||
# Description: View, edit, or reset the SSH login welcome message (/etc/motd)
|
||||
# Author: LXS
|
||||
# Date: 2025
|
||||
|
||||
# Load LXS common library (colors, separator, run_spinner, loggers, helpers)
|
||||
LXS_RAW_BASE="${LXS_RAW_BASE:-https://git.hyko.cx/hykocx/lxs/raw/branch/main}"
|
||||
_lib=$(curl -fsSL "${LXS_RAW_BASE}/lib/common.sh") || { echo "Failed to fetch lib/common.sh" >&2; exit 1; }
|
||||
eval "$_lib"
|
||||
unset _lib
|
||||
export LXS_LOG_FILE="/tmp/lxs_welcome_message.log"
|
||||
|
||||
require_root "$0" "$@"
|
||||
|
||||
set -u
|
||||
|
||||
MOTD_FILE="/etc/motd"
|
||||
BACKUP_FILE="/etc/motd.lxs.bak"
|
||||
DYNAMIC_DIR="/etc/update-motd.d"
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Arguments
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
ACTION=""
|
||||
TEXT=""
|
||||
FROM_FILE=""
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
view|--view|show|--show) ACTION="view" ;;
|
||||
set|--set|edit|--edit) ACTION="set" ;;
|
||||
reset|--reset) ACTION="reset" ;;
|
||||
--text) shift; TEXT=${1:-} ;;
|
||||
--text=*) TEXT="${1#*=}" ;;
|
||||
--from-file) shift; FROM_FILE=${1:-} ;;
|
||||
--from-file=*) FROM_FILE="${1#*=}" ;;
|
||||
-h|--help)
|
||||
cat <<EOF
|
||||
Usage: welcome-message.sh [action] [options]
|
||||
|
||||
Actions:
|
||||
view Show the current welcome message
|
||||
set Set a new welcome message (see source options below)
|
||||
reset Clear the welcome message (a backup is kept)
|
||||
|
||||
Source options for 'set' (mutually exclusive):
|
||||
--text "..." Use the given string as the new message
|
||||
--from-file PATH Read the new message from PATH
|
||||
(none) Open an interactive editor (\$EDITOR or nano/vi)
|
||||
|
||||
-h, --help Show this help
|
||||
|
||||
With no action, an interactive menu is shown.
|
||||
|
||||
The welcome message lives in ${MOTD_FILE}.
|
||||
On Ubuntu, dynamic MOTD scripts in ${DYNAMIC_DIR} also contribute to the
|
||||
banner shown at login — those are not modified by this tool.
|
||||
EOF
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Unknown option: $1${NC}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
if [ -n "$TEXT" ] && [ -n "$FROM_FILE" ]; then
|
||||
err "--text and --from-file are mutually exclusive"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Helpers
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
view_motd() {
|
||||
echo -e "${WHITE}${BOLD}Current welcome message${NC} ${GRAY}(${MOTD_FILE})${NC}"
|
||||
show_separator
|
||||
if [ ! -s "$MOTD_FILE" ]; then
|
||||
echo -e "${GRAY}(empty)${NC}"
|
||||
else
|
||||
cat "$MOTD_FILE"
|
||||
fi
|
||||
show_separator
|
||||
|
||||
if [ -d "$DYNAMIC_DIR" ] && compgen -G "${DYNAMIC_DIR}/*" >/dev/null; then
|
||||
echo ""
|
||||
warn "Dynamic MOTD scripts are present in ${DYNAMIC_DIR}:"
|
||||
find "$DYNAMIC_DIR" -maxdepth 1 -type f -executable -printf ' • %f\n' | sort
|
||||
echo -e "${GRAY}These run at login and add to the banner.${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
backup_motd() {
|
||||
[ -f "$MOTD_FILE" ] || return 0
|
||||
cp -a "$MOTD_FILE" "$BACKUP_FILE"
|
||||
info "Previous message backed up to ${BACKUP_FILE}"
|
||||
}
|
||||
|
||||
write_motd() {
|
||||
local src=$1
|
||||
backup_motd
|
||||
install -m 644 "$src" "$MOTD_FILE"
|
||||
ok "Welcome message updated"
|
||||
echo ""
|
||||
view_motd
|
||||
}
|
||||
|
||||
set_from_text() {
|
||||
local tmp
|
||||
tmp=$(mktemp /tmp/lxs.motd.XXXXXX) || { err "mktemp failed"; return 1; }
|
||||
# Preserve embedded newlines; ensure a trailing newline.
|
||||
printf '%s\n' "$1" > "$tmp"
|
||||
write_motd "$tmp"
|
||||
rm -f "$tmp"
|
||||
}
|
||||
|
||||
set_from_file() {
|
||||
local src=$1
|
||||
if [ ! -r "$src" ]; then
|
||||
err "Cannot read file: ${src}"
|
||||
return 1
|
||||
fi
|
||||
write_motd "$src"
|
||||
}
|
||||
|
||||
set_interactive() {
|
||||
local editor=${EDITOR:-}
|
||||
if [ -z "$editor" ]; then
|
||||
if command -v nano >/dev/null 2>&1; then editor="nano"
|
||||
elif command -v vi >/dev/null 2>&1; then editor="vi"
|
||||
else
|
||||
err "No editor found (\$EDITOR unset; nano/vi missing). Use --text or --from-file."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
local tmp
|
||||
tmp=$(mktemp /tmp/lxs.motd.XXXXXX) || { err "mktemp failed"; return 1; }
|
||||
[ -s "$MOTD_FILE" ] && cat "$MOTD_FILE" > "$tmp"
|
||||
|
||||
info "Opening ${editor}... save and exit to apply, leave empty to cancel."
|
||||
"$editor" "$tmp"
|
||||
|
||||
if [ ! -s "$tmp" ]; then
|
||||
warn "Empty content — change cancelled."
|
||||
rm -f "$tmp"
|
||||
return 0
|
||||
fi
|
||||
|
||||
write_motd "$tmp"
|
||||
rm -f "$tmp"
|
||||
}
|
||||
|
||||
reset_motd() {
|
||||
if [ ! -s "$MOTD_FILE" ]; then
|
||||
info "Welcome message is already empty."
|
||||
return 0
|
||||
fi
|
||||
backup_motd
|
||||
: > "$MOTD_FILE"
|
||||
chmod 644 "$MOTD_FILE"
|
||||
ok "Welcome message cleared"
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Menu (when no action is given on the CLI)
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
if [ -z "$ACTION" ]; then
|
||||
echo -e "${WHITE}${BOLD}LXS - Welcome message${NC}"
|
||||
show_separator
|
||||
echo -e " ${CYAN}[1]${NC} View current message"
|
||||
echo -e " ${GREEN}[2]${NC} Set a new message"
|
||||
echo -e " ${YELLOW}[3]${NC} Reset (clear) the message"
|
||||
echo -e " ${RED}[0]${NC} Cancel"
|
||||
echo ""
|
||||
echo -e -n "${BOLD}Choice [0-3]: ${NC}"
|
||||
read -r choice
|
||||
case "$choice" in
|
||||
1) ACTION="view" ;;
|
||||
2) ACTION="set" ;;
|
||||
3) ACTION="reset" ;;
|
||||
0|"") info "Cancelled."; exit 0 ;;
|
||||
*) err "Invalid option."; exit 1 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
case "$ACTION" in
|
||||
view) view_motd ;;
|
||||
set)
|
||||
if [ -n "$TEXT" ]; then set_from_text "$TEXT"
|
||||
elif [ -n "$FROM_FILE" ]; then set_from_file "$FROM_FILE"
|
||||
else set_interactive
|
||||
fi
|
||||
;;
|
||||
reset) reset_motd ;;
|
||||
esac
|
||||
Reference in New Issue
Block a user