From eadae693a5e62dc0769632bb1a58630887b6ead8 Mon Sep 17 00:00:00 2001 From: Hyko Date: Tue, 12 May 2026 17:29:21 -0400 Subject: [PATCH] feat: initial project scaffold for lxs multi-tool - add main entrypoint with interactive menu and CLI dispatcher (lxs.sh) - add shared helpers library with colors, loggers, and spinner (lib/common.sh) - add app installers for coolify, pterodactyl, uptime-kuma, cloudpanel, and proxmox (apps/) - add system tools for monitoring, benchmarking, and hardening (tools/) - add VERSION file (0.1.0) as single source of truth for releases - add MIT LICENSE - expand README with usage, project structure, and release workflow --- LICENSE | 21 ++ README.md | 86 ++++- VERSION | 1 + apps/cloudpanel.sh | 637 +++++++++++++++++++++++++++++++ apps/coolify.sh | 260 +++++++++++++ apps/proxmox.sh | 354 ++++++++++++++++++ apps/pterodactyl.sh | 330 +++++++++++++++++ apps/uptime-kuma.sh | 248 +++++++++++++ lib/common.sh | 151 ++++++++ lxs.sh | 472 +++++++++++++++++++++++ tools/harden.sh | 211 +++++++++++ tools/server-benchmark.sh | 763 ++++++++++++++++++++++++++++++++++++++ tools/system-tools.sh | 287 ++++++++++++++ 13 files changed, 3820 insertions(+), 1 deletion(-) create mode 100644 LICENSE create mode 100644 VERSION create mode 100755 apps/cloudpanel.sh create mode 100755 apps/coolify.sh create mode 100755 apps/proxmox.sh create mode 100755 apps/pterodactyl.sh create mode 100755 apps/uptime-kuma.sh create mode 100644 lib/common.sh create mode 100755 lxs.sh create mode 100755 tools/harden.sh create mode 100755 tools/server-benchmark.sh create mode 100755 tools/system-tools.sh diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..eb6bd93 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 HYKO + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index f5394db..dfbe74a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,86 @@ -# lxs +# LXS +Linux multi-tool — a Bash menu and CLI for installing common server apps and running system tools on Debian/Ubuntu. + +## Quick install + +```bash +sudo bash <(curl -fsSL https://git.hyko.cx/hykocx/lxs/raw/branch/main/lxs.sh) setup +``` + +This downloads the repo tarball, installs every file to `/usr/local/share/lxs/`, and creates the symlink `/usr/local/bin/lxs`. After install, sub-scripts run from disk — no network calls per command. + +Or run once without installing: + +```bash +bash <(curl -fsSL https://git.hyko.cx/hykocx/lxs/raw/branch/main/lxs.sh) +``` + +When a new version is published in this repo, the interactive menu shows a *"Nouvelle version disponible"* banner; run `lxs update` to pull all updated files. + +## Usage + +```bash +lxs # Interactive menu (checks for updates, cached 24h) +lxs setup # First-time install of all files to /usr/local/share/lxs +lxs update # Update all installed files to latest +lxs install # Install an application +lxs tool [args] # Run a system tool +lxs info # Show system info +lxs version # Show version +lxs help # Show help +``` + +### Applications + +| Command | Description | +|---|---| +| `lxs install coolify` | Self-hosted PaaS | +| `lxs install pterodactyl` | Game server management panel | +| `lxs install uptime-kuma` | Monitoring tool | +| `lxs install cloudpanel` | Web hosting control panel | +| `lxs install proxmox` | Proxmox VE management tools | + +### Tools + +| Command | Description | +|---|---| +| `lxs tool system` | System monitoring and diagnostics | +| `lxs tool benchmark` | Server benchmark (CPU / RAM / disk / network) | +| `lxs tool harden` | Baseline hardening: UFW + fail2ban + SSH key-only + unattended-upgrades | + +## Project structure + +``` +lxs/ +├── lxs.sh # Main entrypoint (menu + CLI dispatcher) +├── VERSION # Single source of truth for the version (bump on every release) +├── lib/ +│ └── common.sh # Shared helpers (colors, loggers, spinner) +├── apps/ # Application installers +│ ├── coolify.sh +│ ├── pterodactyl.sh +│ ├── uptime-kuma.sh +│ ├── cloudpanel.sh +│ └── proxmox.sh +└── tools/ # System tools + ├── system-tools.sh + ├── server-benchmark.sh + └── harden.sh +``` + +After `lxs setup`, the full tree lives in `/usr/local/share/lxs/` and sub-scripts execute from disk. If `lxs.sh` is run without being installed (the one-liner mode), it falls back to downloading sub-scripts on demand. + +### Releasing a new version + +Bump only the [VERSION](VERSION) file — `lxs.sh` reads it at startup. Installed clients also fetch this file (cached 24 h in `~/.cache/lxs/`) to detect updates. In one-shot mode (`bash <(curl …lxs.sh)`) the file isn't readable from the process substitution, so `lxs version` reports `dev`. + +## Requirements + +- Debian or Ubuntu (other distros may work but are not tested) +- `curl` +- Some sub-scripts require root; they will auto-elevate via `sudo` when run. + +## License + +MIT diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.1.0 diff --git a/apps/cloudpanel.sh b/apps/cloudpanel.sh new file mode 100755 index 0000000..bc2e073 --- /dev/null +++ b/apps/cloudpanel.sh @@ -0,0 +1,637 @@ +#!/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. admin@example.com):${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 + + 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}" + + # 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 0 ;; + *) echo -e "${RED}Invalid option${NC}" ;; + esac + + echo "" + read -p "Press Enter to continue..." + done +} + +main + diff --git a/apps/coolify.sh b/apps/coolify.sh new file mode 100755 index 0000000..91a125d --- /dev/null +++ b/apps/coolify.sh @@ -0,0 +1,260 @@ +#!/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 + + 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 + + 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 0 ;; + *) echo -e "${RED}Invalid option${NC}" ;; + esac + + echo "" + read -p "Press Enter to continue..." + done +} + +main diff --git a/apps/proxmox.sh b/apps/proxmox.sh new file mode 100755 index 0000000..cec9a63 --- /dev/null +++ b/apps/proxmox.sh @@ -0,0 +1,354 @@ +#!/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 + + # 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 +} + +# ═══════════════════════════════════════════════════════════════════════════ +# 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 " ${RED}[0]${NC} Back to main menu" + echo "" + echo -n "Choice [0-8]: " +} + +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 ;; + 0) return 0 ;; + *) echo -e "${RED}Invalid option${NC}" ;; + esac + + echo "" + read -p "Press Enter to continue..." + done +} + +main + diff --git a/apps/pterodactyl.sh b/apps/pterodactyl.sh new file mode 100755 index 0000000..e9aa1a5 --- /dev/null +++ b/apps/pterodactyl.sh @@ -0,0 +1,330 @@ +#!/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 < /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 + + 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 + + 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 0 ;; + *) echo -e "${RED}Invalid option${NC}" ;; + esac + + echo "" + read -p "Press Enter to continue..." + done +} + +main diff --git a/apps/uptime-kuma.sh b/apps/uptime-kuma.sh new file mode 100755 index 0000000..68aed63 --- /dev/null +++ b/apps/uptime-kuma.sh @@ -0,0 +1,248 @@ +#!/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 </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 + + 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 <&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 "${PURPLE}[*] ${message}${NC}" + eval "$command" > "$log" 2>&1 & + local pid=$! + + while kill -0 "$pid" 2>/dev/null; do + local temp=${spinstr#?} + printf "\r${PURPLE}[%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${GREEN}[✓]${NC} ${message}\n" + else + printf "\r${RED}[✗]${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 +} + +# 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/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 +} + +# 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}[✗] Timeout waiting for package manager${NC}" && return 1 + + printf "\r${GRAY}[i] Waiting... (%ds/%ds)${NC}" $wait_count $max_wait + sleep 1 + done + + [ "$lock_detected" = true ] && \ + echo -e "\n${GREEN}[✓] Package manager is now available${NC}" + return 0 +} diff --git a/lxs.sh b/lxs.sh new file mode 100755 index 0000000..5b846e8 --- /dev/null +++ b/lxs.sh @@ -0,0 +1,472 @@ +#!/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() { + echo -e "${WHITE}${BOLD}" + echo -e "╔═══════════════════════════════════════════════════════╗" + echo -e "║ ║" + echo -e "║ ██╗ ██╗ ██╗███████╗ ║" + echo -e "║ ██║ ╚██╗██╔╝██╔════╝ ║" + echo -e "║ ██║ ╚███╔╝ ███████╗ ║" + echo -e "║ ██║ ██╔██╗ ╚════██║ ║" + echo -e "║ ███████╗██╔╝ ██╗███████║ ║" + echo -e "║ ╚══════╝╚═╝ ╚═╝╚══════╝ v${LXS_VERSION} ║" + echo -e "║ ║" + echo -e "╚═══════════════════════════════════════════════════════╝" + echo -e "${NC}" +} + +show_header() { + clear + show_lxs_logo + + if [ "${LXS_UPDATE_AVAILABLE:-0}" = "1" ]; then + echo -e "${YELLOW}[!] Nouvelle version disponible: ${LXS_REMOTE_VERSION} (actuelle: ${LXS_VERSION})${NC}" + echo -e "${GRAY} Lance: lxs update${NC}" + echo "" + fi + + local hostname os_version kernel uptime_info ip_address cpu_model 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 || uptime | awk -F'up ' '{print $2}' | awk -F',' '{print $1}') + ip_address=$(get_public_ip) + cpu_model=$(grep "model name" /proc/cpuinfo | head -1 | cut -d':' -f2 | xargs) + 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 -e "${GRAY}Hostname:${NC} $hostname" + echo -e "${GRAY}IP Address:${NC} $ip_address" + echo -e "${GRAY}OS:${NC} $os_version" + echo -e "${GRAY}Kernel:${NC} $kernel" + echo -e "${GRAY}Uptime:${NC} $uptime_info" + echo -e "${GRAY}CPU:${NC} $cpu_model ($cpu_cores cores)" + echo -e "${GRAY}Memory:${NC} $used_mem / $total_mem" + echo -e "${GRAY}Disk:${NC} $disk_usage" + 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}[✓] 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 "${PURPLE}[*] Downloading ${BOLD}${script_name}${NC}${PURPLE}...${NC}" + + if curl -fsSL -H "Cache-Control: no-cache" \ + -o "${temp_file}" \ + "${LXS_RAW_BASE}/${script_path}"; then + echo -e "${GREEN}[✓] Downloaded${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}[✓] Script completed successfully${NC}" + else + echo -e "${YELLOW}[!] Script exited with code: $exit_code${NC}" + fi + return $exit_code + else + echo -e "${RED}[✗] 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 < Install an application + lxs tool [args] Run a system tool + lxs info Show system info + lxs version Show version + lxs help Show this help + +Applications (lxs install ...): + coolify Self-hosted PaaS + pterodactyl Game server management panel + uptime-kuma Monitoring tool + cloudpanel Web hosting control panel + proxmox Proxmox VE management tools + +Tools (lxs tool ...): + system System monitoring and diagnostics + benchmark Server benchmark (CPU/RAM/disk/network) + harden Baseline hardening (UFW + fail2ban + SSH key-only + auto-updates) + +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}[✗] Missing app name. Try: lxs help${NC}"; return 1 ;; + *) echo -e "${RED}[✗] 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-tools.sh" "$@" ;; + benchmark) download_and_run "tools/server-benchmark.sh" "$@" ;; + harden) download_and_run "tools/harden.sh" "$@" ;; + "") echo -e "${RED}[✗] Missing tool name. Try: lxs help${NC}"; return 1 ;; + *) echo -e "${RED}[✗] 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}[✗] Failed to create temp dir${NC}" + return 1 + } + trap 'rm -rf "$work"' RETURN + + echo -e "${PURPLE}[*] Downloading ${LXS_TARBALL_URL}...${NC}" + if ! curl -fsSL -H "Cache-Control: no-cache" -o "${work}/lxs.tgz" "$LXS_TARBALL_URL"; then + echo -e "${RED}[✗] 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}[✗] Extraction failed${NC}" + return 1 + fi + + if [ ! -f "${work}/extracted/lxs.sh" ] || [ ! -f "${work}/extracted/VERSION" ]; then + echo -e "${RED}[✗] 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}[✗] 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}[✗] 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}[✓] lxs ${new_version} installed in ${LXS_INSTALL_DIR}${NC}" + echo -e "${GREEN}[✓] Symlink: ${LXS_BIN_PATH} → ${LXS_INSTALL_DIR}/lxs.sh${NC}" +} + +cmd_update() { + lxs_sync_install update +} + +cmd_install_self() { + lxs_sync_install setup +} + +# ═══════════════════════════════════════════════════════════════════════════ +# Interactive menus +# ═══════════════════════════════════════════════════════════════════════════ + +menu_install_apps() { + while true; do + show_header + echo -e "${WHITE}${BOLD}APPLICATIONS${NC}" + echo "" + echo -e " ${GREEN}[1]${NC} Coolify" + echo -e " ${GREEN}[2]${NC} Pterodactyl Panel" + echo -e " ${GREEN}[3]${NC} Uptime Kuma" + echo -e " ${GREEN}[4]${NC} CloudPanel" + echo -e " ${GREEN}[5]${NC} Proxmox VE Tools" + echo -e " ${RED}[0]${NC} Back" + echo "" + echo -e -n "${BOLD}Choice [0-5]: ${NC}" + read -r choice + + case $choice in + 1) download_and_run "apps/coolify.sh" ;; + 2) download_and_run "apps/pterodactyl.sh" ;; + 3) download_and_run "apps/uptime-kuma.sh" ;; + 4) download_and_run "apps/cloudpanel.sh" ;; + 5) download_and_run "apps/proxmox.sh" ;; + 0) return ;; + *) echo -e "${RED}[✗] Invalid option. Please select 0-5.${NC}"; sleep 1 ;; + esac + + if [ "$choice" != "0" ]; then + echo "" + read -r -p "Press Enter to continue..." + fi + done +} + +main_menu() { + check_os + check_remote_version + while true; do + show_header + echo -e "${WHITE}${BOLD}MAIN MENU${NC}" + echo "" + echo -e " ${GREEN}[1]${NC} Applications" + echo -e " ${CYAN}[2]${NC} System Tools" + echo -e " ${PURPLE}[3]${NC} Server Benchmark" + echo -e " ${YELLOW}[4]${NC} Harden Server" + echo -e " ${GRAY}[u]${NC} Update lxs" + echo -e " ${RED}[0]${NC} Exit" + echo "" + echo -e -n "${BOLD}Choice: ${NC}" + read -r choice + + case $choice in + 1) menu_install_apps ;; + 2) download_and_run "tools/system-tools.sh" ;; + 3) download_and_run "tools/server-benchmark.sh" ;; + 4) download_and_run "tools/harden.sh" ;; + u|U) cmd_update ;; + 0) + clear + show_lxs_logo + echo -e "${GREEN}Bye.${NC}\n" + exit 0 + ;; + *) echo -e "${RED}[✗] Invalid option.${NC}"; sleep 1 ;; + esac + + if [[ "$choice" != "0" && "$choice" != "1" ]]; 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}[✗] Unknown command: $1${NC}"; cmd_help; exit 1 ;; +esac diff --git a/tools/harden.sh b/tools/harden.sh new file mode 100755 index 0000000..0dbcc1d --- /dev/null +++ b/tools/harden.sh @@ -0,0 +1,211 @@ +#!/bin/bash + +# LXS - Server Hardening +# Description: Apply baseline security hardening (UFW + fail2ban + SSH key-only + 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_SSH=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-ssh) DO_SSH=0 ;; + --no-unattended) DO_UNATTENDED=0 ;; + -y|--yes) ASSUME_YES=1 ;; + -h|--help) + cat <&2 + exit 1 + ;; + esac +done + +# ═══════════════════════════════════════════════════════════════════════════ +# Pre-checks +# ═══════════════════════════════════════════════════════════════════════════ + +require_debian_ubuntu || exit 1 + +SSH_PORT=$(awk '/^[[:space:]]*Port[[:space:]]+/ {print $2; exit}' /etc/ssh/sshd_config 2>/dev/null) +SSH_PORT=${SSH_PORT:-22} + +echo -e "${WHITE}${BOLD}LXS Server Hardening${NC}" +echo -e "${GRAY}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +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_SSH -eq 1 ] && echo " • SSH: disable password auth (key-only), prohibit-password root login" +[ $DO_UNATTENDED -eq 1 ] && echo " • unattended-upgrades: enable automatic security updates" +echo -e "${GRAY}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + +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 + +export DEBIAN_FRONTEND=noninteractive + +# ═══════════════════════════════════════════════════════════════════════════ +# 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 <> "$cfg" + grep -q '^PermitRootLogin ' "$cfg" || echo 'PermitRootLogin prohibit-password' >> "$cfg" + + if sshd -t 2>/tmp/lxs_sshd_test; then + systemctl reload ssh 2>/dev/null || systemctl reload sshd + ok "SSH configured for key-only auth (backup: ${cfg}.lxs-backup)" + else + err "sshd config test failed; restoring backup." + cp "${cfg}.lxs-backup" "$cfg" + cat /tmp/lxs_sshd_test >&2 + return 1 + fi +} + +# ═══════════════════════════════════════════════════════════════════════════ +# 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_SSH -eq 1 ] && setup_ssh || true +[ $DO_UNATTENDED -eq 1 ] && setup_unattended + +# ═══════════════════════════════════════════════════════════════════════════ +# Summary +# ═══════════════════════════════════════════════════════════════════════════ + +echo "" +echo -e "${WHITE}${BOLD}Summary${NC}" +echo -e "${GRAY}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +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" +echo "SSH password auth: $(grep -E '^PasswordAuthentication' /etc/ssh/sshd_config 2>/dev/null | awk '{print $2}' || echo unknown)" +echo "SSH root login: $(grep -E '^PermitRootLogin' /etc/ssh/sshd_config 2>/dev/null | awk '{print $2}' || echo unknown)" +systemctl is-active unattended-upgrades >/dev/null 2>&1 && echo "unattended-upgrades: active" || echo "unattended-upgrades: inactive" +echo "" +ok "Hardening complete." diff --git a/tools/server-benchmark.sh b/tools/server-benchmark.sh new file mode 100755 index 0000000..92565ec --- /dev/null +++ b/tools/server-benchmark.sh @@ -0,0 +1,763 @@ +#!/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" + +# ═══════════════════════════════════════════════════════════════════════════ +# 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() { + echo -e "${WHITE}${BOLD}" + echo "╔═══════════════════════════════════════════════════════════════════════════╗" + echo "║ ║" + echo "║ SERVER PERFORMANCE BENCHMARK ║" + echo "║ ║" + echo "╚═══════════════════════════════════════════════════════════════════════════╝" + echo -e "${NC}" +} + +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 "" + + 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//') + + # Test configurations: size_mb, block_size, description + local test_configs=( + "100:1M:100MB Sequential" + "1024:1M:1GB Sequential" + ) + + # Add 2GB test only if we have enough space (at least 3GB free) + if [ "$available_space" -gt 3072 ]; then + test_configs+=("2048:1M:2GB Sequential") + 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 + + # 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//') + + # Test configurations: size_mb, block_size, description + local test_configs=( + "100:1M:100MB Sequential" + "1024:1M:1GB Sequential" + ) + + # Add 2GB test only if we have enough space (at least 3GB free) + if [ "$available_space" -gt 3072 ]; then + test_configs+=("2048:1M:2GB Sequential") + 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) + + # 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 + 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 + diff --git a/tools/system-tools.sh b/tools/system-tools.sh new file mode 100755 index 0000000..a6114a5 --- /dev/null +++ b/tools/system-tools.sh @@ -0,0 +1,287 @@ +#!/bin/bash + +# LXS - System Tools +# 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_tools.log" + +# Menu: System Tools +menu_system_tools() { + while true; do + clear + echo -e "${WHITE}╔═══════════════════════════════════════════════════════╗${NC}" + echo -e "${WHITE}║ SYSTEM TOOLS ║${NC}" + echo -e "${WHITE}╚═══════════════════════════════════════════════════════╝${NC}" + echo "" + echo -e " ${CYAN}[1]${NC} View system informations" + echo -e " ${CYAN}[2]${NC} Check disk space" + echo -e " ${CYAN}[3]${NC} Check memory usage" + echo -e " ${CYAN}[4]${NC} Check CPU load" + echo -e " ${CYAN}[5]${NC} Check network" + echo -e " ${CYAN}[6]${NC} View system logs (last 50 lines)" + echo -e " ${CYAN}[7]${NC} Show top resource-consuming processes" + echo -e " ${CYAN}[8]${NC} Check disk health (SMART)" + echo -e " ${RED}[0]${NC} Exit" + echo "" + echo -e -n "${BOLD}Choice [0-8]: ${NC}" + read -r choice + + echo "" + + case $choice in + 1) + # View system informations + clear + echo -e "${WHITE}╔═══════════════════════════════════════════════════════╗${NC}" + echo -e "${WHITE}║ SYSTEM INFORMATION ║${NC}" + echo -e "${WHITE}╚═══════════════════════════════════════════════════════╝${NC}" + 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 0 + ;; + *) + 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_tools +