#!/usr/bin/python3 import hashlib import io import json import os import random import shutil import socket import subprocess import sys import tarfile import time import urllib.request import zipfile if sys.version_info.major != 3: print("Please run with python3.") sys.exit(1) rPath = os.path.dirname(os.path.realpath(__file__)) # Distribuciones soportadas SUPPORTED_DISTROS = { "ubuntu": ["18.04", "20.04", "22.04", "24.04"], "debian": ["11", "12", "13"], "rocky": ["8", "9"], "almalinux": ["8", "9"], "centos": ["7", "8"], "rhel": ["8", "9"], } PACKAGES = { "debian": [ "iproute2", "net-tools", "dirmngr", "gpg-agent", "software-properties-common", "libcurl4", "libgeoip-dev", "libxslt1-dev", "libonig-dev", "e2fsprogs", "wget", "mariadb-server", "mariadb-client", "sysstat", "alsa-utils", "v4l-utils", "certbot", "iptables-persistent", "libjpeg-dev", "libpng-dev", "libharfbuzz-dev", "libfribidi-dev", "libogg0", "libnuma1", "xz-utils", "zip", "unzip", "libssh2-1", "libsodium23", "cpufrequtils", "mcrypt", "cron", "git", "curl", ], "debian13": [ "iproute2", "net-tools", "dirmngr", "gpg-agent", "software-properties-common", "libcurl4", "wget", "unzip", "zip", "xz-utils", "cron", "git", "sysstat", "perl", "gawk", "socat", "libxml2-dev", "libxslt1-dev", "libonig5", "libonig-dev", "zlib1g-dev", "libssl-dev", "pkg-config", "autoconf", "automake", "alsa-utils", "v4l-utils", "e2fsprogs", "certbot", "iptables-persistent", "libssh2-1", "libssh2-1-dev", "mariadb-server", "mariadb-client", "mariadb-common", "libjpeg-dev", "libpng-dev", "libharfbuzz-dev", "libfribidi-dev", "libgeoip1", "geoip-bin", "libsodium23", "cpufrequtils", "mcrypt", "libogg0", "libnuma1", ], "ubuntu20": [ "iproute2", "net-tools", "dirmngr", "gpg-agent", "software-properties-common", "wget", "curl", "unzip", "zip", "xz-utils", "cron", "git", "sysstat", "ca-certificates", "libcurl4-gnutls-dev", "libxml2-dev", "libxslt1-dev", "libonig5", "libonig-dev", "libjpeg-dev", "libpng-dev", "zlib1g-dev", "alsa-utils", "v4l-utils", "e2fsprogs", "iptables-persistent", "certbot", "python3-certbot", "libssh2-1", "libssh2-1-dev", "mariadb-server", "mariadb-client", "mariadb-common", "libsodium23", "cpufrequtils", "mcrypt", "libogg0", "libnuma1", ], "ubuntu22": [ "iproute2", "net-tools", "dirmngr", "gpg-agent", "software-properties-common", "libcurl4", "libgeoip-dev", "libxslt1-dev", "libonig-dev", "e2fsprogs", "wget", "curl", "unzip", "zip", "xz-utils", "cron", "git", "sysstat", "ca-certificates", "libxml2-dev", "libonig5", "zlib1g-dev", "mariadb-server", "mariadb-client", "mariadb-common", "alsa-utils", "v4l-utils", "certbot", "python3-certbot", "iptables-persistent", "libjpeg-dev", "libpng-dev", "libharfbuzz-dev", "libfribidi-dev", "libogg0", "libnuma1", "libssh2-1", "libssh2-1-dev", "libsodium23", "cpufrequtils", "mcrypt", ], "ubuntu24": [ "iproute2", "net-tools", "dirmngr", "gpg-agent", "software-properties-common", "libcurl4t64", "wget", "unzip", "zip", "xz-utils", "cron", "git", "sysstat", "perl", "gawk", "socat", "libxml2-dev", "libxslt1-dev", "libonig5", "libonig-dev", "zlib1g-dev", "libssl-dev", "pkg-config", "autoconf", "automake", "alsa-utils", "v4l-utils", "e2fsprogs", "certbot", "python3-certbot", "ufw", "libssh2-1t64", "libssh2-1-dev", "mariadb-server", "mariadb-client", "mariadb-common", "libjpeg-dev", "libpng-dev", "libharfbuzz-dev", "libfribidi-dev", "libgeoip1t64", "geoip-bin", "libsodium23", "cpufrequtils", "mcrypt", "libogg0", "libnuma1", ], "debian11": [ "iproute2", "net-tools", "dirmngr", "gpg-agent", "software-properties-common", "libcurl4", "libgeoip-dev", "libxslt1-dev", "libonig-dev", "e2fsprogs", "wget", "curl", "unzip", "zip", "xz-utils", "cron", "git", "sysstat", "mariadb-server", "mariadb-client", "mariadb-common", "alsa-utils", "v4l-utils", "certbot", "iptables-persistent", "libjpeg-dev", "libpng-dev", "libharfbuzz-dev", "libfribidi-dev", "libogg0", "libnuma1", "libssh2-1", "libssh2-1-dev", "libsodium23", "cpufrequtils", "mcrypt", ], "redhat": [ "epel-release", "wget", "mariadb-server", "mariadb", "sysstat", "alsa-utils", "v4l-utils", "libcurl-devel", "geoip-devel", "libxslt-devel", "oniguruma-devel", "e2fsprogs", "libjpeg-turbo-devel", "libpng-devel", "harfbuzz-devel", "fribidi-devel", "libogg", "xz", "zip", "unzip", "libssh2-devel", "cronie", "certbot", "iptables-services", "GeoIP-update", "git", "curl", "libsodium", "numactl", "kernel-tools", ], } rRemove = ["mysql-server"] rMySQLCnfTemplate = """\ # XC_VM [client] port = 3306 [mysqld_safe] nice = 0 [mysqld] user = mysql port = 3306 basedir = /usr datadir = /var/lib/mysql tmpdir = /tmp lc-messages-dir = /usr/share/mysql skip-external-locking skip-name-resolve bind-address = * # MyISAM key_buffer_size = {{KEY_BUFFER}}M myisam_sort_buffer_size = 4M myisam-recover-options = BACKUP max_length_for_sort_data = 4096 # Connections max_connections = {{MAX_CONNECTIONS}} back_log = {{BACK_LOG}} max_connect_errors = 1000 # Packet and cache max_allowed_packet = 16M open_files_limit = 2048 innodb_open_files = 1024 table_open_cache = 1024 table_definition_cache = 1024 # Temp tables tmp_table_size = {{TMP_TABLE_SIZE}}M max_heap_table_size = {{TMP_TABLE_SIZE}}M # InnoDB innodb_buffer_pool_size = {{BUFFER_POOL_SIZE}} innodb_buffer_pool_instances = {{BUFFER_POOL_INSTANCES}} innodb_read_io_threads = 4 innodb_write_io_threads = 4 innodb_flush_log_at_trx_commit = 1 innodb_flush_method = O_DIRECT innodb_file_per_table = 1 innodb_io_capacity = 1000 innodb_table_locks = 1 innodb_lock_wait_timeout = 30 # Logging expire_logs_days = 7 max_binlog_size = 64M # Query cache - disabled query_cache_limit = 0 query_cache_size = 0 query_cache_type = 0 performance_schema = 0 sql_mode = "NO_ENGINE_SUBSTITUTION" [mariadb] thread_cache_size = {{THREAD_CACHE}} thread_handling = pool-of-threads thread_pool_size = 4 thread_pool_idle_timeout = 20 thread_pool_max_threads = {{THREAD_POOL_MAX_THREADS}} [mysqldump] quick quote-names max_allowed_packet = 16M [mysql] [isamchk] key_buffer_size = 8M""" rConfig = """\ ; XC_VM Configuration ; ----------------- ; To change your username or password, modify BOTH ; below and XC_VM will read and re-encrypt them. [XC_VM] hostname = "127.0.0.1" database = "xc_vm" port = 3306 server_id = 1 [Encrypted] username = "%s" password = "%s" """ rSysCtl = """\ # XC_VM net.ipv4.tcp_congestion_control = bbr net.core.default_qdisc = fq net.ipv4.tcp_rmem = 8192 87380 134217728 net.ipv4.udp_rmem_min = 16384 net.core.rmem_default = 262144 net.core.rmem_max = 268435456 net.ipv4.tcp_wmem = 8192 65536 134217728 net.ipv4.udp_wmem_min = 16384 net.core.wmem_default = 262144 net.core.wmem_max = 268435456 net.core.somaxconn = 1000000 net.core.netdev_max_backlog = 250000 net.core.optmem_max = 65535 net.ipv4.tcp_max_tw_buckets = 1440000 net.ipv4.tcp_max_orphans = 16384 net.ipv4.ip_local_port_range = 2000 65000 net.ipv4.tcp_no_metrics_save = 1 net.ipv4.tcp_slow_start_after_idle = 0 net.ipv4.tcp_fin_timeout = 15 net.ipv4.tcp_keepalive_time = 300 net.ipv4.tcp_keepalive_probes = 5 net.ipv4.tcp_keepalive_intvl = 15 fs.file-max=20970800 fs.nr_open=20970800 fs.aio-max-nr=20970800 net.ipv4.tcp_timestamps = 1 net.ipv4.tcp_window_scaling = 1 net.ipv4.tcp_mtu_probing = 1 net.ipv4.route.flush = 1 net.ipv6.route.flush = 1""" rSystemd = """\ [Unit] SourcePath=/home/xc_vm/service Description=XC_VM Service After=network.target StartLimitIntervalSec=0 [Service] Type=simple User=root Restart=always RestartSec=1 LimitNOFILE=655350 TimeoutStopSec=30 KillMode=mixed ExecStart=/bin/bash /home/xc_vm/service start ExecStop=/bin/bash /home/xc_vm/service stop ExecReload=/bin/bash /home/xc_vm/service restart [Install] WantedBy=multi-user.target""" rChoice = "23456789abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ" rConfigPath = "/home/xc_vm/config/config.ini" class col: HEADER = "\033[95m" OKBLUE = "\033[94m" OKGREEN = "\033[92m" WARNING = "\033[93m" FAIL = "\033[91m" ENDC = "\033[0m" BOLD = "\033[1m" UNDERLINE = "\033[4m" # Check if running as root def check_root(): """Check if script is running as root""" if os.geteuid() != 0: printc("This script must be run as root.", col.FAIL) printc("Please use: su -c 'python3 your_script_name.py'", col.OKBLUE) sys.exit(1) # Install prerequisites def install_prerequisites(dist_info): """Install prerequisites like sudo and curl if they are missing""" if dist_info["family"] == "debian": printc("Checking for prerequisites (sudo, curl)...", col.OKBLUE) # Check if sudo exists ret, _, _ = run_command("which sudo", capture_output=True) sudo_missing = ret != 0 # Check if curl exists ret, _, _ = run_command("which curl", capture_output=True) curl_missing = ret != 0 if sudo_missing or curl_missing: printc("Installing missing prerequisites...", col.WARNING) printc("Updating package lists...", col.OKBLUE) run_command("apt-get update -y") packages_to_install = [] if sudo_missing: packages_to_install.append("sudo") if curl_missing: packages_to_install.append("curl") if packages_to_install: packages_str = " ".join(packages_to_install) printc(f"Installing {packages_str}...", col.OKBLUE) run_command(f"apt-get install -y {packages_str}") printc("Prerequisites installed successfully.", col.OKGREEN) else: printc("Prerequisites (sudo, curl) are already installed.", col.OKGREEN) elif dist_info["family"] == "redhat": printc("Checking for prerequisites (sudo, curl, wget)...", col.OKBLUE) ret, _, _ = run_command("which sudo", capture_output=True) sudo_missing = ret != 0 ret, _, _ = run_command("which curl", capture_output=True) curl_missing = ret != 0 ret, _, _ = run_command("which wget", capture_output=True) wget_missing = ret != 0 if sudo_missing or curl_missing or wget_missing: printc("Installing missing prerequisites...", col.WARNING) packages_to_install = [] if sudo_missing: packages_to_install.append("sudo") if curl_missing: packages_to_install.append("curl") if wget_missing: packages_to_install.append("wget") if packages_to_install: packages_str = " ".join(packages_to_install) printc(f"Installing {packages_str}...", col.OKBLUE) run_command( f"yum install -y {packages_str} || dnf install -y {packages_str}" ) printc("Prerequisites installed successfully.", col.OKGREEN) else: printc("Prerequisites are already installed.", col.OKGREEN) def compute_md5(file_path): """Compute MD5 hash of a file""" md5 = hashlib.md5() with open(file_path, "rb") as f: for chunk in iter(lambda: f.read(8192), b""): md5.update(chunk) return md5.hexdigest() def download_release_hash(repo, release_tag, file_name): """Download hashes.md5 from a GitHub release and return the hash for file_name""" hash_url = f"https://github.com/Vateron-Media/{repo}/releases/download/{release_tag}/hashes.md5" try: req = urllib.request.Request(hash_url) req.add_header("User-Agent", "XC_VM-Installer/1.0") with urllib.request.urlopen(req, timeout=15) as response: content = response.read().decode().strip() for line in content.split("\n"): line = line.strip() if not line: continue parts = line.split(None, 1) if len(parts) == 2 and parts[1] == file_name: return parts[0] except Exception as e: printc(f"Warning: Could not download hash file: {e}", col.WARNING) return None def get_latest_binaries_tag(): """Get the latest release tag from XC_VM_Binaries GitHub repo""" api_url = ( "https://api.github.com/repos/Vateron-Media/XC_VM_Binaries/releases/latest" ) try: req = urllib.request.Request(api_url) req.add_header("User-Agent", "XC_VM-Installer/1.0") with urllib.request.urlopen(req, timeout=15) as response: data = json.loads(response.read().decode()) return data["tag_name"] except Exception as e: printc(f"Failed to get latest binaries release tag: {e}", col.WARNING) return None def write_bin_version_file(release_tag, asset_name, dist_id, version): """Write installed binaries release metadata to /home/xc_vm/bin/bin_version.json""" version_path = "/home/xc_vm/bin/bin_version.json" payload = { "owner": "Vateron-Media", "repository": "XC_VM_Binaries", "release": release_tag, "asset": asset_name, "distribution": dist_id, "distribution_version": version, "updated_at_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), } try: with open(version_path, "w", encoding="utf-8") as f: json.dump(payload, f, indent=4) f.write("\n") try: run_command(f"chown xc_vm:xc_vm {version_path}") except Exception: pass printc( f"Binaries version file updated: {version_path} ({release_tag})", col.OKGREEN, ) return True except Exception as e: printc(f"Warning: Failed to update {version_path}: {e}", col.WARNING) return False # Install distribution-specific binaries def install_distribution_binaries(dist_id, version): """Download and install distribution-specific binaries from GitHub releases""" printc(f"Installing {dist_id} {version} specific binaries...", col.OKBLUE) # Determinar el nombre del archivo en GitHub releases major = version.split(".")[0] if dist_id == "ubuntu": if major in ["18", "20", "22", "24"]: patch_name = f"ubuntu_{major}.tar.gz" display_name = f"Ubuntu {major}" else: printc(f"Ubuntu version {version} not supported for patches", col.WARNING) return False elif dist_id == "debian": if major in ["11", "12", "13"]: patch_name = f"debian_{major}.tar.gz" display_name = f"Debian {major}" else: printc(f"Debian version {version} not supported for patches", col.WARNING) return False elif dist_id in ["rocky", "almalinux", "rhel", "centos"]: if major in ["8", "9"]: patch_name = f"rhel_{major}.tar.gz" display_name = f"RHEL/Rocky/Alma {major}" else: printc( f"{dist_id} version {version} not supported for patches", col.WARNING ) return False else: printc(f"Distribution {dist_id} not supported for patches", col.WARNING) return False # Get latest release tag from GitHub release_tag = get_latest_binaries_tag() if not release_tag: printc("Could not determine latest binaries release, skipping", col.WARNING) return False remote_url = f"https://github.com/Vateron-Media/XC_VM_Binaries/releases/download/{release_tag}/{patch_name}" temp_tar_file = f"/tmp/{patch_name}" temp_extract_dir = f"/tmp/{patch_name.replace('.tar.gz', '_extract')}" try: # Always download fresh binaries from GitHub printc( f"Downloading {display_name} binaries from GitHub release {release_tag}...", col.OKBLUE, ) printc(f"URL: {remote_url}", col.OKBLUE) # Remove old file if exists if os.path.exists(temp_tar_file): os.remove(temp_tar_file) # Retry on transient network/DNS failures (e.g. resolver not ready yet). last_err = None for attempt in range(1, 5): try: urllib.request.urlretrieve(remote_url, temp_tar_file) last_err = None break except Exception as e: last_err = e printc(f"Download attempt {attempt}/4 failed: {e}", col.WARNING) time.sleep(5) if last_err is not None: raise last_err if not (os.path.exists(temp_tar_file) and os.path.getsize(temp_tar_file) > 0): printc(f"Failed to download {display_name} binaries", col.FAIL) return False # MD5 verification expected_hash = download_release_hash("XC_VM_Binaries", release_tag, patch_name) if expected_hash: actual_hash = compute_md5(temp_tar_file) if actual_hash != expected_hash: printc(f"MD5 verification failed for {patch_name}: expected {expected_hash}, got {actual_hash}", col.FAIL) os.remove(temp_tar_file) return False printc(f"MD5 verification passed for {patch_name}", col.OKGREEN) else: printc(f"Warning: Could not retrieve MD5 hash for {patch_name}, skipping verification", col.WARNING) file_size_mb = os.path.getsize(temp_tar_file) / (1024 * 1024) printc( f"{display_name} binaries downloaded: {file_size_mb:.1f} MB", col.OKGREEN ) # 2. Clean previous extraction directory if exists if os.path.exists(temp_extract_dir): shutil.rmtree(temp_extract_dir) # 3. Extract to temporary directory printc( f"Extracting {display_name} binaries to temporary location...", col.OKBLUE ) with tarfile.open(temp_tar_file, "r:gz") as tar: tar.extractall( path=temp_extract_dir, members=_safe_tar_members(tar, temp_extract_dir) ) # Auto-detect directory structure major = version.split(".")[0] # Variants: debian11, debian_11, ubuntu20, ubuntu_20 distro_variants = [ f"{dist_id}{major}", f"{dist_id}_{major}", ] # Possible paths where binaries might be possible_paths = [] for dname in distro_variants: possible_paths.append(os.path.join(temp_extract_dir, dname, "bin")) possible_paths.append(os.path.join(temp_extract_dir, dname)) possible_paths.append(os.path.join(temp_extract_dir, "bin")) possible_paths.append(temp_extract_dir) source_bin_dir = None for path in possible_paths: if os.path.exists(path): # Check if it contains binary files or typical directories contents = os.listdir(path) has_binaries = any( item in contents for item in ["php", "nginx", "nginx_rtmp", "bin"] ) if has_binaries or path.endswith("/bin"): source_bin_dir = path printc(f"Structure found: {source_bin_dir}", col.OKGREEN) break # If not found in expected paths, search recursively if not source_bin_dir: printc("Searching directory structure recursively...", col.OKBLUE) for root, dirs, files in os.walk(temp_extract_dir): # Search for directories containing typical binaries if any(item in dirs for item in ["php", "nginx", "nginx_rtmp", "bin"]): source_bin_dir = root printc( f"Estructura encontrada recursivamente: {source_bin_dir}", col.OKGREEN, ) break target_bin_dir = "/home/xc_vm/bin" if not source_bin_dir: printc( "Error: No se pudo encontrar la estructura de binarios en el parche.", col.FAIL, ) printc( f"Contenido de {temp_extract_dir}: {os.listdir(temp_extract_dir)}", col.WARNING, ) # Show full structure for debugging printc("Full structure of extracted directory:", col.WARNING) for root, dirs, files in os.walk(temp_extract_dir): level = root.replace(temp_extract_dir, "").count(os.sep) indent = " " * 2 * level printc(f"{indent}{os.path.basename(root)}/", col.WARNING) subindent = " " * 2 * (level + 1) for file in files[:10]: # Limit to 10 files to avoid clutter printc(f"{subindent}{file}", col.WARNING) if len(files) > 10: printc( f"{subindent}... and {len(files) - 10} more files", col.WARNING ) return False # 4. Replace specific files, not the entire directory printc( f"Replacing specific binaries with {display_name} versions...", col.OKBLUE, ) # Recursively walk source directory files for root, dirs, files in os.walk(source_bin_dir): # Calculate relative path from source directory rel_path = os.path.relpath(root, source_bin_dir) # Create corresponding target directories if they don't exist if rel_path != ".": target_dir = os.path.join(target_bin_dir, rel_path) os.makedirs(target_dir, exist_ok=True) # Copy files for file in files: source_file = os.path.join(root, file) if rel_path == ".": target_file = os.path.join(target_bin_dir, file) else: target_file = os.path.join(target_bin_dir, rel_path, file) # Create directories if needed os.makedirs(os.path.dirname(target_file), exist_ok=True) # Copy file (overwriting if exists) shutil.copy2(source_file, target_file) # printc(f"Updated: {target_file}", col.OKBLUE) printc(f"{display_name} binaries updated successfully", col.OKGREEN) # 5. Set permissions on key executables printc(f"Setting permissions for {display_name} binaries...", col.OKBLUE) executables_to_chmod = [ "/home/xc_vm/bin/php/bin/php", "/home/xc_vm/bin/php/sbin/php-fpm", "/home/xc_vm/bin/nginx/sbin/nginx", "/home/xc_vm/bin/nginx_rtmp/sbin/nginx_rtmp", ] for exe_path in executables_to_chmod: if os.path.exists(exe_path): run_command(f"chmod +x {exe_path}") # 6. Clean up temporary files printc("Cleaning up temporary files...", col.OKBLUE) if os.path.exists(temp_tar_file): os.remove(temp_tar_file) if os.path.exists(temp_extract_dir): shutil.rmtree(temp_extract_dir) write_bin_version_file(release_tag, patch_name, dist_id, version) printc( f"{display_name} specific binary installation completed", col.OKGREEN, ) return True except Exception as e: printc(f"Error installing {display_name} binaries: {e}", col.FAIL) # Clean up archive files on error if os.path.exists(temp_tar_file): os.remove(temp_tar_file) if os.path.exists(temp_extract_dir): shutil.rmtree(temp_extract_dir) return False def detect_distribution(): """Detect Linux distribution and version without external modules""" dist_id = "unknown" version = "unknown" family = "unknown" # Try /etc/os-release first (standard method) if os.path.exists("/etc/os-release"): try: with open("/etc/os-release", "r") as f: lines = f.readlines() for line in lines: line = line.strip() if line.startswith("ID="): dist_id = line.split("=")[1].strip().strip('"') elif line.startswith("VERSION_ID="): version = line.split("=")[1].strip().strip('"') except Exception: pass # Try older methods if dist_id == "unknown": if os.path.exists("/etc/redhat-release"): dist_id = "centos" try: with open("/etc/redhat-release", "r") as f: content = f.read().lower() if "rocky" in content: dist_id = "rocky" elif "alma" in content: dist_id = "almalinux" elif "rhel" in content: dist_id = "rhel" elif "fedora" in content: dist_id = "fedora" except Exception: pass elif os.path.exists("/etc/debian_version"): dist_id = "debian" try: with open("/etc/debian_version", "r") as f: version = f.read().strip() except Exception: pass elif os.path.exists("/etc/lsb-release"): try: with open("/etc/lsb-release", "r") as f: lines = f.readlines() for line in lines: if line.startswith("DISTRIB_ID="): dist_id = line.split("=")[1].strip().lower().strip('"') elif line.startswith("DISTRIB_RELEASE="): version = line.split("=")[1].strip().strip('"') except Exception: pass # Determine family if dist_id in ["centos", "rhel", "rocky", "almalinux", "fedora"]: family = "redhat" elif dist_id in ["ubuntu", "debian"]: family = "debian" else: family = dist_id return { "id": dist_id, "family": family, "version": version, "full_version": version, } def check_supported_distro(dist_info): """Check if distribution is supported""" dist_id = dist_info["id"] version = dist_info["version"] if dist_id in SUPPORTED_DISTROS: if version in SUPPORTED_DISTROS[dist_id]: return True else: # Check if any supported version starts with the same major version for supported_version in SUPPORTED_DISTROS[dist_id]: if supported_version.startswith(version.split(".")[0]): return True # If not in list but is a known family, we'll try anyway if dist_info["family"] in ["debian", "redhat"]: return True return False def run_command(cmd, shell=True, capture_output=False): """Run shell command with error handling""" try: if capture_output: result = subprocess.run(cmd, shell=shell, capture_output=True, text=True) return result.returncode, result.stdout, result.stderr else: result = subprocess.run(cmd, shell=shell) return result.returncode, None, None except Exception as e: return 1, None, str(e) def generate_self_signed_cert(): """Generate a fresh, per-install self-signed TLS certificate for nginx. The archive ships a placeholder bin/nginx/conf/server.{crt,key}; reusing it would mean every install shares the same private key. Here we overwrite it with a freshly generated unique key/cert BEFORE nginx ever starts. Certbot later replaces this with a real Let's Encrypt certificate (CertbotCronJob), but until then nginx serves this self-signed pair. """ conf_dir = "/home/xc_vm/bin/nginx/conf" key_path = conf_dir + "/server.key" crt_path = conf_dir + "/server.crt" common_name = socket.gethostname() or "xc_vm" printc("Generating a unique self-signed TLS certificate...", col.OKBLUE) rc, _, err = run_command( "openssl req -x509 -newkey rsa:2048 -nodes " '-keyout "' + key_path + '" -out "' + crt_path + '" ' '-days 3650 -subj "/CN=' + common_name + '"', capture_output=True, ) if rc != 0 or not os.path.exists(key_path) or not os.path.exists(crt_path): printc("Failed to generate self-signed certificate: " + str(err), col.FAIL) sys.exit(1) run_command('chown xc_vm:xc_vm "' + key_path + '" "' + crt_path + '"') run_command('chmod 640 "' + key_path + '"') run_command('chmod 644 "' + crt_path + '"') printc("Self-signed certificate generated.", col.OKGREEN) def _safe_tar_members(tar, dest): """Filter tar members to prevent path traversal attacks (CVE-2007-4559)""" dest = os.path.realpath(dest) for member in tar.getmembers(): member_path = os.path.realpath(os.path.join(dest, member.name)) if not member_path.startswith(dest + os.sep) and member_path != dest: printc(f"Skipping unsafe tar member: {member.name}", col.WARNING) continue yield member def is_valid_zip(file_path): """Check if a file is a valid ZIP archive""" try: with zipfile.ZipFile(file_path, "r") as zip_ref: # Try to list contents zip_ref.namelist() return True except Exception: return False def is_valid_tar(file_path): """Check if a file is a valid TAR archive""" try: with tarfile.open(file_path, "r:*") as tar_ref: # Try to list contents tar_ref.getmembers() return True except Exception: return False def download_xc_vm(): """Download XC_VM from GitHub releases""" printc("Checking for XC_VM installation files...", col.OKBLUE) # Check if valid files already exist locally if os.path.exists("./xc_vm.tar.gz") and is_valid_tar("./xc_vm.tar.gz"): printc("Valid xc_vm.tar.gz found locally", col.OKGREEN) return True if os.path.exists("./XC_VM.zip") and is_valid_zip("./XC_VM.zip"): printc("Valid XC_VM.zip found locally", col.OKGREEN) return True printc("Not found. Trying to download from GitHub...", col.OKBLUE) try: # 1. Get latest version from GitHub API printc("Getting latest version from GitHub API...", col.OKBLUE) api_url = "https://api.github.com/repos/Vateron-Media/XC_VM/releases/latest" req = urllib.request.Request(api_url) req.add_header("User-Agent", "XC_VM-Installer/1.0") with urllib.request.urlopen(req, timeout=10) as response: data = json.loads(response.read().decode()) latest_version = data["tag_name"] printc(f"Latest version: {latest_version}", col.OKGREEN) # 2. Download XC_VM.zip directly from GitHub releases download_url = f"https://github.com/Vateron-Media/XC_VM/releases/download/{latest_version}/XC_VM.zip" printc(f"Downloading: {download_url}", col.OKBLUE) # Download using urllib urllib.request.urlretrieve(download_url, "XC_VM.zip") # Verify download if os.path.exists("XC_VM.zip") and os.path.getsize("XC_VM.zip") > 0: file_size = os.path.getsize("XC_VM.zip") printc(f"Download successful: XC_VM.zip ({file_size} bytes)", col.OKGREEN) # MD5 verification expected_hash = download_release_hash("XC_VM", latest_version, "XC_VM.zip") if expected_hash: actual_hash = compute_md5("XC_VM.zip") if actual_hash != expected_hash: printc(f"MD5 verification failed for XC_VM.zip: expected {expected_hash}, got {actual_hash}", col.FAIL) os.remove("XC_VM.zip") return False printc("MD5 verification passed for XC_VM.zip", col.OKGREEN) else: printc("Warning: Could not retrieve MD5 hash for XC_VM.zip, skipping verification", col.WARNING) # Validate ZIP file if is_valid_zip("XC_VM.zip"): printc("ZIP archive validated successfully", col.OKGREEN) return True else: printc("Downloaded file is not a valid ZIP archive", col.WARNING) os.remove("XC_VM.zip") return False else: printc("Download failed or file is empty", col.FAIL) return False except Exception as e: printc(f"Download error: {e}", col.FAIL) # Try alternative methods if download fails printc("Trying alternative download methods...", col.WARNING) # Alternative method: Direct download from latest try: alt_url = "https://github.com/Vateron-Media/XC_VM/releases/latest/download/XC_VM.zip" printc(f"Trying alternative: {alt_url}", col.OKBLUE) urllib.request.urlretrieve(alt_url, "XC_VM.zip") if os.path.exists("XC_VM.zip") and is_valid_zip("XC_VM.zip"): printc("Alternative download successful", col.OKGREEN) return True except Exception: pass return False def install_mariadb_repo(dist_info): """Install MariaDB repository based on distribution""" dist_id = dist_info["id"] version = dist_info["version"] family = dist_info["family"] printc(f"Configuring MariaDB repository for {dist_id} {version}", col.OKBLUE) if family == "debian": # Debian/Ubuntu if dist_id == "ubuntu": # Try to get codename from /etc/os-release codename = "jammy" # Default for Ubuntu 22.04 try: with open("/etc/os-release", "r") as f: for line in f: if line.startswith("UBUNTU_CODENAME="): codename = line.split("=")[1].strip().strip('"') break elif line.startswith("VERSION_CODENAME="): codename = line.split("=")[1].strip().strip('"') break except Exception: # Fallback based on version if version.startswith("20"): codename = "focal" elif version.startswith("22"): codename = "jammy" elif version.startswith("24"): codename = "noble" printc(f"Using Ubuntu codename: {codename}", col.OKGREEN) # Install prerequisites run_command( "apt-get install -y apt-transport-https curl gnupg software-properties-common" ) # Special handling for Ubuntu 20.04 LTS (EOL) if dist_id == "ubuntu" and version.startswith("20"): printc( "Ubuntu 20.04 LTS detected, using system MariaDB packages...", col.WARNING, ) printc( "MariaDB 11.4 is not compatible with Ubuntu 20.04 (libc6 incompatibility)", col.OKBLUE, ) printc( "Using Ubuntu 20.04 default MariaDB packages for compatibility", col.OKGREEN, ) # For Ubuntu 20.04, we'll use the system MariaDB packages # Ubuntu 20.04 default repositories have MariaDB 10.3 which is compatible try: # Remove any existing MariaDB repository files if os.path.exists("/etc/apt/sources.list.d/mariadb.list"): os.remove("/etc/apt/sources.list.d/mariadb.list") printc("Removed incompatible MariaDB repository", col.OKBLUE) if os.path.exists("/usr/share/keyrings/mariadb.gpg"): os.remove("/usr/share/keyrings/mariadb.gpg") printc( "Will use Ubuntu 20.04 default MariaDB packages (10.3.x)", col.OKGREEN, ) # Skip external repository setup for Ubuntu 20.04 except Exception as e: printc(f"Error cleaning up MariaDB repositories: {e}", col.WARNING) printc("Continuing with system packages...", col.WARNING) else: # For other versions, use the official script printc( "Adding MariaDB repository using official setup script...", col.OKBLUE ) ret, _, _ = run_command( "curl -LsS https://r.mariadb.com/downloads/mariadb_repo_setup | bash -s -- --mariadb-server-version='mariadb-11.4'", capture_output=True, ) if ret != 0: printc( "MariaDB repo setup script failed, will use system packages", col.WARNING, ) # Clean up any partial repo configuration run_command( "rm -f /etc/apt/sources.list.d/mariadb.list " "/etc/apt/sources.list.d/mariadb.sources || true" ) # Add MaxMind repository for Ubuntu (with error handling) if dist_id == "ubuntu": printc("Adding MaxMind repository for GeoIP...", col.OKBLUE) try: run_command("add-apt-repository -y ppa:maxmind/ppa") printc("MaxMind repository added successfully", col.OKGREEN) except Exception as e: printc(f"MaxMind PPA failed to add: {e}", col.WARNING) printc( "Continuing without MaxMind PPA (not critical for functionality)", col.OKBLUE, ) else: printc("Skipping MaxMind PPA (not available for Debian)", col.OKBLUE) # Update package list printc("Updating package list...", col.OKBLUE) run_command("apt-get update") elif family == "redhat": # RedHat based distributions run_command("yum install -y curl") # Install MariaDB repository using official script printc("Adding MariaDB repository...", col.OKBLUE) run_command( "curl -LsS https://r.mariadb.com/downloads/mariadb_repo_setup | bash -s -- --mariadb-server-version='mariadb-11.4'" ) else: printc(f"Unsupported distribution: {dist_id}", col.WARNING) return False return True def secure_mariadb_installation(root_password, dist_info=None): """Secure MariaDB installation with root password (from bash script)""" printc("Securing MariaDB installation", col.OKBLUE) # Check if MariaDB is running ret, out, err = run_command("systemctl is-active mariadb", capture_output=True) if ret != 0: printc("Starting MariaDB service", col.OKBLUE) run_command("systemctl start mariadb") time.sleep(5) # Get MariaDB version to determine syntax printc("Checking MariaDB version...", col.OKBLUE) version_cmd = "mariadb --version 2>/dev/null | head -n 1 || mysql --version 2>/dev/null | head -n 1" ret, out, err = run_command(version_cmd, capture_output=True) mariadb_version = out.strip() if out else "" is_mariadb_103 = ("10.3" in mariadb_version or "5.7" in mariadb_version or "5.6" in mariadb_version) if is_mariadb_103: printc("MariaDB 10.3 detected, using legacy password syntax", col.OKBLUE) else: printc("MariaDB 11.x detected, using modern password syntax", col.OKBLUE) # Determine authentication plugin printc("Checking MariaDB authentication plugin...", col.OKBLUE) auth_cmd = "mariadb -u root -e \"SELECT plugin FROM mysql.user WHERE User='root' AND Host='localhost';\" 2>/dev/null | tail -n +2" ret, out, err = run_command(auth_cmd, capture_output=True) auth_plugin = out.strip() if out else "" if ret == 0 and auth_plugin == "unix_socket": printc( "Using unix_socket authentication, converting to password...", col.OKBLUE ) # Convert from unix_socket to password authentication with version-specific syntax if is_mariadb_103: # MariaDB 10.3 and older syntax sql_commands = [ "FLUSH PRIVILEGES;", f"SET PASSWORD FOR 'root'@'localhost' = PASSWORD('{root_password}');", "DELETE FROM mysql.user WHERE User='';", "DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');", "DROP DATABASE IF EXISTS test;", "DELETE FROM mysql.db WHERE Db='test' OR Db='test\\\\_%';", "FLUSH PRIVILEGES;", ] else: # MariaDB 10.4+ and MariaDB 11.x syntax sql_commands = [ "FLUSH PRIVILEGES;", f"ALTER USER 'root'@'localhost' IDENTIFIED VIA mysql_native_password USING PASSWORD('{root_password}');", "DELETE FROM mysql.user WHERE User='';", "DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');", "DROP DATABASE IF EXISTS test;", "DELETE FROM mysql.db WHERE Db='test' OR Db='test\\\\_%';", "FLUSH PRIVILEGES;", ] for sql in sql_commands: if is_mariadb_103 and "SET PASSWORD" in sql: # Special handling for SET PASSWORD command run_command(f'mariadb -u root -e "{sql}"', shell=True) else: run_command(f'mariadb -u root -e "{sql}"') printc("MariaDB secured with password authentication", col.OKGREEN) else: printc("Setting MariaDB root password...", col.OKBLUE) # Try to set password with version-specific syntax if is_mariadb_103: # MariaDB 10.3 syntax set_cmd = f"mariadb -u root -e \"SET PASSWORD FOR 'root'@'localhost' = PASSWORD('{root_password}');\" 2>/dev/null || true" else: # MariaDB 11.x syntax set_cmd = f"mariadb -u root -e \"ALTER USER 'root'@'localhost' IDENTIFIED BY '{root_password}';\" 2>/dev/null || true" run_command(set_cmd) # Create custom security configuration (like 99-custom.cnf from bash script) printc("Creating custom MariaDB security configuration...", col.OKBLUE) custom_conf = """[mysqld] bind-address = 0.0.0.0 skip-name-resolve local-infile = 0 symbolic-links = 0 slow_query_log = 1 slow_query_log_file = /var/log/mysql/mariadb-slow.log long_query_time = 2 log_error = /var/log/mysql/error.log""" conf_dir = "/etc/mysql/mariadb.conf.d/" if not os.path.exists(conf_dir): conf_dir = "/etc/my.cnf.d/" if not os.path.exists(conf_dir): os.makedirs(conf_dir, exist_ok=True) custom_path = os.path.join(conf_dir, "99-custom.cnf") with open(custom_path, "w") as f: f.write(custom_conf) # Create log directory and set permissions run_command("mkdir -p /var/log/mysql && chown mysql:mysql /var/log/mysql") # Restart MariaDB run_command("systemctl restart mariadb") time.sleep(3) # Don't create /root/mariadb_root_password.txt, only use /root/credentials.txt printc("MariaDB security hardening completed", col.OKGREEN) return root_password def get_system_ram_mb(): """Get total system RAM in MB""" try: with open("/proc/meminfo", "r") as f: for line in f: if line.startswith("MemTotal:"): mem_kb = int(line.split()[1]) return mem_kb // 1024 # Convert to MB except Exception: pass return 1024 # Value by default if unable to determine def generate_mysql_config(total_ram_mb): """Generate MySQL configuration based on total RAM (from bash script logic)""" # Calculate based on RAM (similar to bash script) buffer_pool_mb = int(total_ram_mb * 0.25) if total_ram_mb < 512: buffer_pool_mb = 64 max_connections = 40 elif total_ram_mb < 1024: if buffer_pool_mb > 128: buffer_pool_mb = 128 max_connections = 80 elif total_ram_mb < 2048: if buffer_pool_mb > 256: buffer_pool_mb = 256 max_connections = 120 elif total_ram_mb < 4096: if buffer_pool_mb > 512: buffer_pool_mb = 512 max_connections = 200 elif total_ram_mb < 8192: if buffer_pool_mb > 1024: buffer_pool_mb = 1024 max_connections = 300 elif total_ram_mb < 16384: if buffer_pool_mb > 2048: buffer_pool_mb = 2048 max_connections = 450 else: if buffer_pool_mb > 4096: buffer_pool_mb = 4096 max_connections = 600 # Format buffer pool size if buffer_pool_mb >= 1024: buffer_pool_size = f"{buffer_pool_mb // 1024}G" else: buffer_pool_size = f"{buffer_pool_mb}M" # Calculate other values key_buffer = min(buffer_pool_mb // 8, 32) tmp_table_size = min(buffer_pool_mb // 4, 64) back_log = min(max(max_connections // 2, 128), 1024) thread_cache = min(max(max_connections // 4, 32), 256) thread_pool_max_threads = min(max(max_connections, 256), 1024) buffer_pool_instances = "1" if buffer_pool_mb < 1024 else "2" # Generate config from template config = rMySQLCnfTemplate config = config.replace("{{KEY_BUFFER}}", str(key_buffer)) config = config.replace("{{MAX_CONNECTIONS}}", str(max_connections)) config = config.replace("{{BACK_LOG}}", str(back_log)) config = config.replace("{{TMP_TABLE_SIZE}}", str(tmp_table_size)) config = config.replace("{{BUFFER_POOL_SIZE}}", buffer_pool_size) config = config.replace("{{BUFFER_POOL_INSTANCES}}", buffer_pool_instances) config = config.replace("{{THREAD_CACHE}}", str(thread_cache)) config = config.replace("{{THREAD_POOL_MAX_THREADS}}", str(thread_pool_max_threads)) printc(f"RAM detected: {total_ram_mb}MB", col.OKGREEN) printc( f"Buffer pool configured: {buffer_pool_size} ({buffer_pool_mb}MB)", col.OKGREEN ) printc(f"Max connections: {max_connections}", col.OKGREEN) printc(f"Thread pool max threads: {thread_pool_max_threads}", col.OKGREEN) return config def generate_random_password(length=32): """Generate random password (similar to bash script)""" chars = "23456789abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ" return "".join(random.choice(chars) for _ in range(length)) def generate_root_password(): """Generate secure root password of 20 characters (SQL/shell safe)""" # Only use characters safe for SQL literals and shell quoting chars = "23456789abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ" return "".join(random.choice(chars) for _ in range(20)) def sanitize_password_for_sql(password): """Escape single quotes for safe use in SQL strings""" return password.replace("'", "''") def sanitize_password_for_shell(password): """Escape password for safe use in shell double-quoted strings""" # Escape characters special to bash inside double quotes for ch in ("\\", '"', "$", "`", "!"): password = password.replace(ch, "\\" + ch) return password def getIP(): try: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.connect(("8.8.8.8", 80)) ip = s.getsockname()[0] s.close() return ip except Exception: # Fallback method try: hostname = socket.gethostname() ip = socket.gethostbyname(hostname) if ip and ip != "127.0.0.1": return ip except Exception: pass return "127.0.0.1" def printc(rText, rColour=col.OKBLUE, rPadding=0): rLeft = int(30 - (len(rText) / 2)) rRight = 60 - rLeft - len(rText) print( "%s |--------------------------------------------------------------| %s" % (rColour, col.ENDC) ) for i in range(rPadding): print( "%s | | %s" % (rColour, col.ENDC) ) print("%s | %s%s%s | %s" % (rColour, " " * rLeft, rText, " " * rRight, col.ENDC)) for i in range(rPadding): print( "%s | | %s" % (rColour, col.ENDC) ) print( "%s |--------------------------------------------------------------| %s" % (rColour, col.ENDC) ) print(" ") def extract_archive(archive_path): """Extract archive with proper validation and error handling""" printc(f"Extracting {archive_path}...", col.OKBLUE) if archive_path.endswith(".tar.gz") or archive_path.endswith(".tgz"): try: with tarfile.open(archive_path, "r:gz") as tar: # Get member list for debugging members = tar.getmembers() printc( f"Archive contains {len(members)} files/directories", col.OKGREEN ) # Extract with path traversal protection safe = list(_safe_tar_members(tar, "/home/xc_vm/")) tar.extractall(path="/home/xc_vm/", members=safe) printc("Extraction successful", col.OKGREEN) return True except Exception as e: printc(f"Failed to extract tar.gz: {e}", col.FAIL) return False elif archive_path.endswith(".zip"): try: with zipfile.ZipFile(archive_path, "r") as zip_ref: # Get file list for debugging file_list = zip_ref.namelist() printc(f"Archive contains {len(file_list)} files", col.OKGREEN) # Extract all files zip_ref.extractall(path="/home/xc_vm/") printc("Extraction successful", col.OKGREEN) # Check if zip contains nested tar.gz for file in file_list: if file.endswith("xc_vm.tar.gz") or file.endswith(".tar.gz"): nested_path = os.path.join("/home/xc_vm", file) if os.path.exists(nested_path): printc( f"Found nested archive: {file}, extracting...", col.OKBLUE, ) return extract_archive(nested_path) return True except Exception as e: printc(f"Failed to extract zip: {e}", col.FAIL) return False else: printc(f"Unsupported archive format: {archive_path}", col.FAIL) return False def fix_ssh2_library_issue(): """Fix SSH2 library issue by creating proper symlinks""" printc("Configuring SSH2 libraries for PHP", col.OKBLUE) # List of possible libssh2.so.1 locations libssh2_paths = [ "/usr/lib/x86_64-linux-gnu/libssh2.so.1", "/usr/lib/x86_64-linux-gnu/libssh2.so.1.0.1", "/usr/lib/x86_64-linux-gnu/libssh2.so", "/usr/lib64/libssh2.so.1", "/usr/lib/libssh2.so.1", "/usr/local/lib/libssh2.so.1", ] found_lib = None for lib_path in libssh2_paths: if os.path.exists(lib_path): found_lib = lib_path printc(f"Found SSH2 library: {lib_path}", col.OKGREEN) break # If not found in static list, search dynamically (handles t64 suffix on 24.04+) if not found_lib: try: ret, out, err = run_command( "find /usr/lib -name 'libssh2.so*' -type f -o -name 'libssh2.so*' -type l 2>/dev/null | head -5", capture_output=True, ) if ret == 0 and out and out.strip(): found_lib = out.strip().split("\n")[0] printc(f"Found SSH2 library via search: {found_lib}", col.OKGREEN) except Exception: pass if found_lib: # Create symlinks in common locations symlink_targets = [ "/usr/lib/libssh2.so.1", "/usr/local/lib/libssh2.so.1", "/lib/libssh2.so.1", ] for target in symlink_targets: if not os.path.exists(target): try: run_command(f"ln -sf {found_lib} {target}") printc(f"Created symlink: {found_lib} -> {target}", col.OKGREEN) except Exception: printc(f"Failed to create symlink for {target}", col.WARNING) # Also check if we need to symlink in PHP extensions directory php_ext_dir = "/home/xc_vm/bin/php/lib/php/extensions/" if os.path.exists(php_ext_dir): # Find the actual extensions directory for dirpath, dirnames, filenames in os.walk(php_ext_dir): if "ssh2.so" in filenames: ssh2_so_path = os.path.join(dirpath, "ssh2.so") printc(f"Found PHP ssh2.so at: {ssh2_so_path}", col.OKGREEN) break # Update dynamic linker cache run_command("ldconfig 2>/dev/null || true") else: printc( "Warning: libssh2.so.1 not found. SSH2 may not work properly.", col.WARNING ) # Don't disable ssh2 extension - only create symlinks # Mapping: (dist_id, major_version) -> PACKAGES key _PACKAGE_KEY_MAP = { ("ubuntu", "18"): "ubuntu20", ("ubuntu", "20"): "ubuntu20", ("ubuntu", "22"): "ubuntu22", ("ubuntu", "24"): "ubuntu24", ("debian", "11"): "debian11", ("debian", "12"): "debian", ("debian", "13"): "debian13", } # Distros that need OpenSSL 3 compatibility library _NEEDS_OPENSSL3 = {"ubuntu_18", "ubuntu_20", "debian_11"} def _install_openssl3_compat(dist_id, major): """Download and install OpenSSL 3 compatibility .deb for older distros.""" label = f"{dist_id}_{major}" tmp_file = f"/tmp/libssl3_{label}.deb" printc("Installing OpenSSL 3 compatibility library for PHP binaries...", col.OKBLUE) try: run_command( f"wget -qO {tmp_file} " '"http://security.ubuntu.com/ubuntu/pool/main/o/openssl/libssl3_3.0.2-0ubuntu1_amd64.deb"' ) run_command( f"dpkg --force-depends -i {tmp_file} 2>/dev/null " f"|| dpkg -i {tmp_file} 2>/dev/null || true" ) run_command(f"rm -f {tmp_file}") printc("OpenSSL 3 library installation completed", col.OKGREEN) except Exception as e: printc(f"OpenSSL 3 installation warning: {e}", col.WARNING) printc("PHP binaries may not work without libssl.so.3", col.WARNING) def _apt_install_framed(packages): """Install packages one at a time, each under its own frame. For every package a printc frame names what is being installed, and apt's raw log streams straight to the terminal below that frame (no capture_output), so the operator sees which package is installing and its full output. Returns the list of packages whose install returned a non-zero code. """ failed = [] total = len(packages) for idx, pkg in enumerate(packages, 1): printc(f"Installing package {idx}/{total}: {pkg}", col.OKBLUE) # No capture_output → apt's log is shown directly under the frame. ret, _, _ = run_command( f"DEBIAN_FRONTEND=noninteractive apt-get -yq install {pkg}" ) if ret != 0: printc(f"Package failed: {pkg}", col.WARNING) failed.append(pkg) return failed def install_deb_packages(dist_info): """Install Debian/Ubuntu packages based on detected distribution.""" dist_id = dist_info["id"] version = dist_info["version"] major = version.split(".")[0] label = f"{dist_id.capitalize()} {version}" package_key = _PACKAGE_KEY_MAP.get((dist_id, major), "debian") printc(f"Using {label} compatible package installation", col.OKBLUE) # Fix broken packages first printc("Fixing any broken packages...", col.OKBLUE) run_command("apt --fix-broken install -y || true") run_command("apt-get autoremove -y || true") run_command("apt-get autoclean || true") # Install OpenSSL 3 compat for older distros ssl3_key = f"{dist_id}_{major}" if ssl3_key in _NEEDS_OPENSSL3: _install_openssl3_compat(dist_id, major) # Install packages packages = PACKAGES.get(package_key, []) if packages: # Separate MariaDB packages from the rest to avoid atomic apt-get failure _mariadb_pkgs = {"mariadb-server", "mariadb-client", "mariadb-common"} db_packages = [p for p in packages if p in _mariadb_pkgs] other_packages = [p for p in packages if p not in _mariadb_pkgs] # Install non-DB packages one by one: a frame per package, log shown below. if other_packages: printc( f"Installing {label} system packages ({len(other_packages)})...", col.OKBLUE, ) failed = _apt_install_framed(other_packages) if failed: printc( "These packages failed to install: " + ", ".join(failed), col.WARNING, ) # Install MariaDB packages separately with fallback if db_packages: db_str = " ".join(db_packages) printc( f"Installing MariaDB packages: {db_str}", col.OKBLUE ) # No capture_output → apt's log is shown directly under the frame. ret, _, _ = run_command( f"DEBIAN_FRONTEND=noninteractive apt-get -yq install {db_str}" ) if ret != 0: printc( "MariaDB from configured repo failed, trying system packages...", col.WARNING, ) # Remove broken MariaDB repo and retry with system packages run_command( "rm -f /etc/apt/sources.list.d/mariadb.list " "/etc/apt/sources.list.d/mariadb.sources || true" ) run_command("apt-get update -y") ret2, _, _ = run_command( f"DEBIAN_FRONTEND=noninteractive apt-get -yq install {db_str}", capture_output=True, ) if ret2 != 0: printc( "CRITICAL: MariaDB packages could not be installed!", col.FAIL, ) # For unknown distros, also try installing SSH2 libraries if (dist_id, major) not in _PACKAGE_KEY_MAP: printc("Installing SSH2 libraries...", col.OKBLUE) run_command( "apt-get install -y libssh2-1 libssh2-1-dev " "|| apt-get install -y libssh2-1 libssh2-1t64 || true" ) # Fix broken packages after installation printc("Final fix for any remaining broken packages...", col.OKBLUE) run_command("apt --fix-broken install -y || true") if __name__ == "__main__": ################################################## # START # ################################################## printc("XC_VM Multi-Distribution Installer", col.OKGREEN, 2) # Check root check_root() # Detect distribution dist_info = detect_distribution() printc( f"Detected: {dist_info['id']} {dist_info['version']} ({dist_info['family']} family)", col.OKGREEN, ) # Install prerequisites before continuing install_prerequisites(dist_info) if not check_supported_distro(dist_info): printc( f"Warning: {dist_info['id']} {dist_info['version']} is not officially supported", col.WARNING, ) response = input("Continue anyway? (Y/N): ").strip().upper() if response != "Y": sys.exit(1) printc("Continuing with installation...", col.WARNING) # Try to download XC_VM if not present has_valid_archive = False archive_path = None # Check for existing valid archives if os.path.exists("./xc_vm.tar.gz") and is_valid_tar("./xc_vm.tar.gz"): has_valid_archive = True archive_path = "./xc_vm.tar.gz" printc("Found valid xc_vm.tar.gz", col.OKGREEN) elif os.path.exists("./XC_VM.zip") and is_valid_zip("./XC_VM.zip"): has_valid_archive = True archive_path = "./XC_VM.zip" printc("Found valid XC_VM.zip", col.OKGREEN) # Download if needed if not has_valid_archive: if download_xc_vm(): # Check what was downloaded if os.path.exists("./xc_vm.tar.gz") and is_valid_tar("./xc_vm.tar.gz"): has_valid_archive = True archive_path = "./xc_vm.tar.gz" elif os.path.exists("./XC_VM.zip") and is_valid_zip("./XC_VM.zip"): has_valid_archive = True archive_path = "./XC_VM.zip" if not has_valid_archive or not archive_path: printc( "XC_VM package not found or invalid. Please download manually.", col.FAIL ) printc( "You can download from: https://github.com/Vateron-Media/XC_VM/releases", col.OKBLUE, ) sys.exit(1) rHost = "127.0.0.1" rServerID = 1 rUsername = generate_random_password(32) # Username de 32 caracteres rPassword = generate_random_password(32) # Password de 32 caracteres rDatabase = "xc_vm" rPort = 3306 # Ask for MariaDB root password (or generate) printc("MariaDB Root Password Configuration", col.OKBLUE) print("For security, you should set a strong root password for MariaDB.") print("Leave empty to generate a random password.") root_password = input( "MariaDB root password (or press Enter to generate): " ).strip() if not root_password: root_password = generate_root_password() printc(f"Generated root password: {root_password}", col.OKGREEN) else: # Reject characters that break SQL/shell quoting forbidden = set("'\"`$;") if forbidden & set(root_password): printc( "Password contains unsafe characters (' \" ` $ ;). Generating a safe one.", col.WARNING, ) root_password = generate_root_password() printc(f"Generated root password: {root_password}", col.OKGREEN) elif len(root_password) < 12: printc( f"Warning: Password is only {len(root_password)} chars. Minimum recommended: 12.", col.WARNING, ) response = input("Continue with provided password? (Y/N): ").strip().upper() if response != "Y": root_password = generate_root_password() printc(f"Using generated root password: {root_password}", col.OKGREEN) else: printc("Using provided root password", col.OKGREEN) if os.path.exists("/home/xc_vm/"): printc("XC_VM Directory Exists!", col.WARNING) while True: rAnswer = input("Continue and overwrite? (Y / N) : ").strip().upper() if rAnswer in ["Y", "N"]: break if rAnswer == "N": sys.exit(1) ################################################## # SYSTEM PREPARATION # ################################################## printc("Preparing System", col.OKBLUE) if dist_info["family"] == "debian": # Debian/Ubuntu printc("Cleaning package locks", col.OKBLUE) for rFile in [ "/var/lib/dpkg/lock-frontend", "/var/cache/apt/archives/lock", "/var/lib/dpkg/lock", "/var/lib/apt/lists/lock", ]: if os.path.exists(rFile): try: os.remove(rFile) except Exception: pass printc("Updating system", col.OKBLUE) run_command("apt-get update -y") # Install MariaDB repository if not install_mariadb_repo(dist_info): printc("Using system MariaDB repository", col.WARNING) # Stop conflicting services printc("Stopping conflicting services (Apache/System Nginx)...", col.OKBLUE) run_command("systemctl stop apache2 nginx 2>/dev/null || true") run_command("systemctl disable apache2 nginx 2>/dev/null || true") # Remove conflicting packages for rPackage in rRemove: printc(f"Removing {rPackage}", col.OKBLUE) run_command(f"apt-get remove {rPackage} -y") # Install packages printc("Installing system packages", col.OKBLUE) install_deb_packages(dist_info) # Verify MariaDB was actually installed ret, _, _ = run_command("dpkg -l mariadb-server 2>/dev/null | grep -q '^ii'", capture_output=True) if ret != 0: printc("MariaDB not installed after package step, retrying...", col.WARNING) run_command("apt-get update -y") ret2, _, _ = run_command( "DEBIAN_FRONTEND=noninteractive apt-get -yq install mariadb-server mariadb-client", capture_output=True, ) if ret2 != 0: printc("CRITICAL: Could not install MariaDB!", col.FAIL) sys.exit(1) # Enable MariaDB run_command("systemctl enable mariadb") # Fix SSH2 library issue printc("Configuring SSH2 libraries", col.OKBLUE) fix_ssh2_library_issue() elif dist_info["family"] == "redhat": # RedHat based distributions printc("Configuring repositories", col.OKBLUE) # Install EPEL run_command("yum install -y epel-release") # Install MariaDB repository if not install_mariadb_repo(dist_info): printc("Using system MariaDB repository", col.WARNING) printc("Updating system", col.OKBLUE) run_command("yum update -y") # Install system packages from PACKAGES['redhat'] redhat_packages = PACKAGES.get("redhat", []) if redhat_packages: packages_str = " ".join(redhat_packages) printc( f"Installing RedHat packages ({len(redhat_packages)} packages)...", col.OKBLUE, ) run_command( f"yum install -y {packages_str} || echo 'Some packages may not be available'" ) # Ensure libssh2 libraries are installed printc("Verifying SSH2 libraries for RedHat...", col.OKBLUE) run_command("yum install -y libssh2 libssh2-devel || true") # Enable services run_command("systemctl enable mariadb") run_command("systemctl enable crond") # Fix SSH2 library issue printc("Configuring SSH2 libraries", col.OKBLUE) fix_ssh2_library_issue() else: printc(f"Unsupported distribution family: {dist_info['family']}", col.FAIL) sys.exit(1) # Create user if doesn't exist printc("Creating/verifying xc_vm user", col.OKBLUE) try: ret, out, err = run_command("getent passwd xc_vm", capture_output=True) if ret == 0: printc("User xc_vm already exists", col.OKGREEN) else: raise Exception("User not found") except Exception: printc("Creating user xc_vm", col.OKBLUE) # capture_output=True keeps adduser/useradd's raw "info:" lines out of the # terminal so the framed printc messages stay clean and consistent. if dist_info["family"] == "debian": rc, _, err = run_command( "adduser --system --shell /bin/false --no-create-home --home /nonexistent --group --disabled-login xc_vm", capture_output=True, ) else: # redhat run_command("groupadd -r xc_vm", capture_output=True) rc, _, err = run_command( "useradd -r -g xc_vm -s /bin/false -M -d /nonexistent xc_vm", capture_output=True, ) if rc == 0: printc("User xc_vm created", col.OKGREEN) else: printc("Failed to create user xc_vm: " + str(err), col.FAIL) sys.exit(1) if not os.path.exists("/home/xc_vm"): os.makedirs("/home/xc_vm", exist_ok=True) run_command("chown xc_vm:xc_vm /home/xc_vm") ################################################## # INSTALL XC_VM # ################################################## printc("Installing XC_VM", col.OKBLUE) # Extract the archive if not extract_archive(archive_path): printc("Failed to extract archive! Exiting", col.FAIL) sys.exit(1) # Verify extraction if not os.path.exists("/home/xc_vm/console.php"): printc("Extraction failed: /home/xc_vm/console.php not found", col.FAIL) sys.exit(1) else: printc("XC_VM extracted successfully", col.OKGREEN) # Replace the shipped placeholder cert with a unique per-install one # (before nginx is started further below). generate_self_signed_cert() # Install distribution-specific binaries for supported distros dist_id = dist_info["id"] version = dist_info["version"] # Check if distribution has patches available if dist_id in ["ubuntu", "debian"]: # Ubuntu: 20, 22, 24 if dist_id == "ubuntu" and any( version.startswith(v) for v in ["20", "22", "24"] ): if not install_distribution_binaries(dist_id, version): printc( f"FATAL: failed to install {dist_id} {version} binaries — the panel " f"cannot run without them. Check network/DNS and re-run the installer.", col.FAIL, ) sys.exit(1) # Debian: 11, 12, 13 elif dist_id == "debian" and any( version.startswith(v) for v in ["11", "12", "13"] ): if not install_distribution_binaries(dist_id, version): printc( f"FATAL: failed to install {dist_id} {version} binaries — the panel " f"cannot run without them. Check network/DNS and re-run the installer.", col.FAIL, ) sys.exit(1) else: printc( f"No specific patches available for {dist_id} {version}, using default binaries", col.OKBLUE, ) elif dist_id in ["rocky", "almalinux", "rhel", "centos"]: major = version.split(".")[0] if major in ["8", "9"]: if not install_distribution_binaries(dist_id, version): printc( f"FATAL: failed to install {dist_id} {version} binaries — the panel " f"cannot run without them. Check network/DNS and re-run the installer.", col.FAIL, ) sys.exit(1) else: printc( f"No specific patches available for {dist_id} {version}, using default binaries", col.OKBLUE, ) else: printc( f"No patches available for {dist_id} {version}, using default binaries", col.OKBLUE, ) ################################################## # MariaDB CONFIGURATION # ################################################## printc("Configuring MariaDB", col.OKBLUE) # Secure MariaDB installation (using bash script logic) secure_mariadb_installation(root_password, dist_info) # Get total system RAM and generate config total_ram_mb = get_system_ram_mb() rMySQLCnf = generate_mysql_config(total_ram_mb) # Write MySQL configuration printc("Writing MySQL performance configuration", col.OKBLUE) if dist_info["family"] == "debian": mysql_conf_path = "/etc/mysql/mariadb.conf.d/50-server.cnf" else: mysql_conf_path = "/etc/my.cnf.d/server.cnf" # Ensure directory exists os.makedirs(os.path.dirname(mysql_conf_path), exist_ok=True) with io.open(mysql_conf_path, "w", encoding="utf-8") as rFile: rFile.write(rMySQLCnf) # Restart MariaDB run_command("systemctl restart mariadb") time.sleep(5) # Connect to MariaDB and configure databases printc("Setting up databases and users", col.OKBLUE) # Create databases run_command( f'mariadb -u root -p"{root_password}" -e "CREATE DATABASE IF NOT EXISTS xc_vm; CREATE DATABASE IF NOT EXISTS xc_vm_migrate;"' ) # Import database schema printc("Importing database schema", col.OKBLUE) db_schema_path = "/home/xc_vm/bin/install/database.sql" if os.path.exists(db_schema_path): run_command(f'mariadb -u root -p"{root_password}" xc_vm < "{db_schema_path}"') else: printc(f"Database schema not found at {db_schema_path}", col.WARNING) # Create XC_VM user with all privileges printc("Creating database user", col.OKBLUE) # Localhost grants commands_localhost = [ f"CREATE USER IF NOT EXISTS '{rUsername}'@'localhost' IDENTIFIED BY '{rPassword}';", f"GRANT ALL PRIVILEGES ON xc_vm.* TO '{rUsername}'@'localhost';", f"GRANT ALL PRIVILEGES ON xc_vm_migrate.* TO '{rUsername}'@'localhost';", f"GRANT ALL PRIVILEGES ON mysql.* TO '{rUsername}'@'localhost';", f"GRANT GRANT OPTION ON xc_vm.* TO '{rUsername}'@'localhost';", ] # 127.0.0.1 grants (REQUIRED for startup.php) commands_127 = [ f"CREATE USER IF NOT EXISTS '{rUsername}'@'127.0.0.1' IDENTIFIED BY '{rPassword}';", f"GRANT ALL PRIVILEGES ON xc_vm.* TO '{rUsername}'@'127.0.0.1';", f"GRANT ALL PRIVILEGES ON xc_vm_migrate.* TO '{rUsername}'@'127.0.0.1';", f"GRANT ALL PRIVILEGES ON mysql.* TO '{rUsername}'@'127.0.0.1';", f"GRANT GRANT OPTION ON xc_vm.* TO '{rUsername}'@'127.0.0.1';", "FLUSH PRIVILEGES;", ] all_commands = commands_localhost + commands_127 for cmd in all_commands: run_command(f'mariadb -u root -p"{root_password}" -e "{cmd}"') # Write XC_VM configuration printc("Writing XC_VM configuration", col.OKBLUE) os.makedirs(os.path.dirname(rConfigPath), exist_ok=True) rConfigData = rConfig % (rUsername, rPassword) with io.open(rConfigPath, "w", encoding="utf-8") as rFile: rFile.write(rConfigData) # Per-install OPENSSL_EXTRA secret — key material for Encryption (hmac_keys, # cached image filenames, proxy URL keys, stream tokens). Generated ONCE and # never rotated: rotating it would make all existing encrypted data # undecryptable, so only create the file when absent (re-running the installer # must never overwrite it). PHP reads it via ConstantsInitializer, falling # back to the historical literal when the file is missing. rOpensslExtraPath = "/home/xc_vm/config/openssl_extra" if not os.path.exists(rOpensslExtraPath): with io.open(rOpensslExtraPath, "w", encoding="utf-8") as rFile: rFile.write(generate_random_password(40)) os.chmod(rOpensslExtraPath, 0o600) printc("MariaDB configuration completed", col.OKGREEN) ################################################## # SYSTEM CONFIGURATION # ################################################## printc("Configuring System", col.OKBLUE) # Configure tmpfs mounts if not os.path.exists("/etc/fstab"): printc("/etc/fstab not found", col.WARNING) else: try: with open("/etc/fstab", "r") as f: fstab_content = f.read() if "/home/xc_vm/" not in fstab_content: printc("Adding tmpfs mounts to /etc/fstab", col.OKBLUE) # Create directories first run_command("mkdir -p /home/xc_vm/content/streams") run_command("mkdir -p /home/xc_vm/tmp") with io.open("/etc/fstab", "a", encoding="utf-8") as rFile: rFile.write( "\ntmpfs /home/xc_vm/content/streams tmpfs defaults,noatime,nosuid,nodev,noexec,mode=1777,size=90% 0 0\ntmpfs /home/xc_vm/tmp tmpfs defaults,noatime,nosuid,nodev,noexec,mode=1777,size=6G 0 0" ) # Mount immediately run_command("mount -a") printc("Reloading systemd to recognize fstab changes", col.OKBLUE) run_command("systemctl daemon-reload") except Exception as e: printc(f"Error updating /etc/fstab: {e}", col.WARNING) # Remove any restrictive sudoers rules sudoers_file = "/etc/sudoers.d/xc_vm" if os.path.exists(sudoers_file): run_command(f"rm -f {sudoers_file}") # Configure HTTP/HTTPS ports printc("Port Configuration", col.OKBLUE) printc("Enter new port values, or leave empty for defaults", col.OKBLUE) while True: http_port = input("HTTP port (default 80): ").strip() if not http_port: http_port = "80" break if http_port.isdigit() and 1 <= int(http_port) <= 65535: break printc("Error: port must be a number between 1 and 65535", col.FAIL) while True: https_port = input("HTTPS port (default 443): ").strip() if not https_port: https_port = "443" break if https_port.isdigit() and 1 <= int(https_port) <= 65535: break printc("Error: port must be a number between 1 and 65535", col.FAIL) # Write HTTP ports configuration http_conf_path = "/home/xc_vm/bin/nginx/conf/ports/http.conf" os.makedirs(os.path.dirname(http_conf_path), exist_ok=True) with io.open(http_conf_path, "w", encoding="utf-8") as rFile: rFile.write(f"listen {http_port};") # Write HTTPS ports configuration https_conf_path = "/home/xc_vm/bin/nginx/conf/ports/https.conf" os.makedirs(os.path.dirname(https_conf_path), exist_ok=True) with io.open(https_conf_path, "w", encoding="utf-8") as rFile: rFile.write(f"listen {https_port} ssl;") printc(f"Ports configured: HTTP - {http_port}, HTTPS - {https_port}", col.OKGREEN) # Update Main Server broadcast ports in database run_command( f'mariadb -u root -p"{root_password}" xc_vm -e ' f'"UPDATE servers SET http_broadcast_port={int(http_port)}, https_broadcast_port={int(https_port)} WHERE is_main=1;"' ) printc("Main Server broadcast ports updated in database", col.OKGREEN) # Configure sysctl print( "Custom sysctl.conf - If you have your own custom sysctl.conf, type N or it will be overwritten. If you don't know what a sysctl configuration is, type Y as it will correctly set your TCP settings and open file limits." ) print(" ") while True: rAnswer = input("Overwrite sysctl configuration? Recommended! (Y / N): ") if rAnswer.upper() in ["Y", "N"]: break if rAnswer.upper() == "Y": try: run_command("modprobe ip_conntrack 2>/dev/null || true") except Exception: pass try: with io.open("/etc/sysctl.conf", "w", encoding="utf-8") as rFile: rFile.write(rSysCtl) run_command("sysctl -p > /dev/null 2>&1") with open("/home/xc_vm/config/sysctl.on", "w") as rFile: pass except Exception: printc("Failed to write to sysctl file.", col.WARNING) else: if os.path.exists("/home/xc_vm/config/sysctl.on"): os.remove("/home/xc_vm/config/sysctl.on") # Configure systemd limits printc("Configuring systemd file limits", col.OKBLUE) systemd_conf = "/etc/systemd/system.conf" if os.path.exists(systemd_conf): with open(systemd_conf, "r") as f: systemd_content = f.read() if "DefaultLimitNOFILE=655350" not in systemd_content: with open(systemd_conf, "a") as f: f.write("\nDefaultLimitNOFILE=655350\n") user_conf = "/etc/systemd/user.conf" if os.path.exists(user_conf): with open(user_conf, "r") as f: user_content = f.read() if "DefaultLimitNOFILE=655350" not in user_content: with open(user_conf, "a") as f: f.write("\nDefaultLimitNOFILE=655350\n") # Configure systemd service (always, like etalon) printc("Configuring systemd service", col.OKBLUE) if os.path.exists("/etc/init.d/xc_vm"): os.remove("/etc/init.d/xc_vm") if os.path.exists("/etc/systemd/system/xc_vm.service"): os.remove("/etc/systemd/system/xc_vm.service") service_path = "/etc/systemd/system/xc_vm.service" with io.open(service_path, "w", encoding="utf-8") as rFile: rFile.write(rSystemd) run_command("chmod +x /etc/systemd/system/xc_vm.service") run_command("systemctl daemon-reload") run_command("systemctl enable xc_vm") ################################################## # ACCESS CODE # ################################################## printc("Generating access code", col.OKBLUE) rCodeDir = "/home/xc_vm/bin/nginx/conf/codes/" # Ensure access codes directory exists os.makedirs(rCodeDir, exist_ok=True) admin_code = None if os.path.exists(rCodeDir): for filename in os.listdir(rCodeDir): if filename.endswith(".conf"): filepath = os.path.join(rCodeDir, filename) if filename.split(".")[0] == "setup": os.remove(filepath) else: try: with open(filepath, "r") as f: content = f.read() if "/home/xc_vm/admin" in content: admin_code = filename.split(".")[0] break except Exception: pass if not admin_code: admin_code = generate_random_password(8) printc(f"Generated access code: {admin_code}", col.OKGREEN) # Insert into database insert_cmd = f"mariadb -u root -p\"{root_password}\" -e \"USE xc_vm; INSERT INTO access_codes(code, type, enabled, groups) VALUES('{admin_code}', 0, 1, '[1]');\"" run_command(insert_cmd) # Create nginx configuration template_path = os.path.join(rCodeDir, "template") if os.path.exists(template_path): with open(template_path, "r") as f: template_content = f.read() # Replace placeholders template_content = template_content.replace("#WHITELIST#", "") template_content = template_content.replace("#TYPE#", "admin") template_content = template_content.replace("#CODE#", admin_code) template_content = template_content.replace("#BURST#", "500") code_conf_path = os.path.join(rCodeDir, f"{admin_code}.conf") with io.open(code_conf_path, "w", encoding="utf-8") as rFile: rFile.write(template_content) printc(f"Access code configuration created: {admin_code}.conf", col.OKGREEN) else: printc("Template file not found, creating basic configuration", col.WARNING) # Fallback configuration fallback_config = f"location /{admin_code} {{ include /home/xc_vm/bin/nginx/conf/proxy.conf; proxy_pass http://127.0.0.1:8080/admin; }}" code_conf_path = os.path.join(rCodeDir, f"{admin_code}.conf") with io.open(code_conf_path, "w", encoding="utf-8") as rFile: rFile.write(fallback_config) else: printc(f"Using existing access code: {admin_code}", col.OKGREEN) ################################################## # FINAL CONFIGURATION # ################################################## printc("Finalizing installation", col.OKBLUE) # Set permissions run_command("chown -R xc_vm:xc_vm /home/xc_vm") # Set executable permissions on key files run_command("chmod +x /home/xc_vm/service") run_command("chmod +x /home/xc_vm/console.php") run_command("chmod +x /home/xc_vm/bin/nginx/sbin/nginx") # Check for RTMP nginx nginx_rtmp_path = "/home/xc_vm/bin/nginx_rtmp/sbin/nginx_rtmp" if os.path.exists(nginx_rtmp_path): run_command(f"chmod +x {nginx_rtmp_path}") # Set capabilities for binding to privileged ports nginx_bin = "/home/xc_vm/bin/nginx/sbin/nginx" if os.path.exists(nginx_bin): run_command( f"setcap 'cap_net_bind_service=+ep' {nginx_bin} 2>/dev/null || true" ) if os.path.exists(nginx_rtmp_path): run_command( f"setcap 'cap_net_bind_service=+ep' {nginx_rtmp_path} 2>/dev/null || true" ) # Save credentials in the format you requested printc("Saving credentials", col.OKBLUE) # Save to /root/credentials.txt with io.open("/root/credentials.txt", "w", encoding="utf-8") as rFile: rFile.write("MariaDB Root \n") rFile.write("Username: root\n") rFile.write(f"Password: {root_password}\n\n") rFile.write(f"XC_VM Username: {rUsername}\n") rFile.write(f"XC_VM Password: {rPassword}\n") rFile.write(f"Database: {rDatabase}\n") # Also save to installer directory (like original install script) local_creds_path = os.path.join(rPath, "credentials.txt") with io.open(local_creds_path, "w", encoding="utf-8") as rFile: rFile.write(f"MariaDB Root Password: {root_password}\n") rFile.write(f"MariaDB Username: {rUsername}\n") rFile.write(f"MariaDB Password: {rPassword}\n") rFile.write(f"Database: {rDatabase}\n") rFile.write(f"Admin Access Code: {admin_code}\n") # Remove the old mariadb_root_password.txt file if it exists if os.path.exists("/root/mariadb_root_password.txt"): os.remove("/root/mariadb_root_password.txt") printc("Credentials saved to /root/credentials.txt", col.OKGREEN) printc(f"Credentials also saved to {local_creds_path}", col.OKGREEN) # Mount tmpfs filesystems (like original install script) run_command("mount -a >/dev/null 2>&1 || true") # Reload systemd daemon run_command("systemctl daemon-reload") # Start service run_command("systemctl start xc_vm") # Post-install startup printc("Starting XC_VM processes...", col.OKBLUE) time.sleep(10) # Run status command (root by design: root crontab, system limits, DB # migrations — the command refuses to run as any other user). if os.path.exists("/home/xc_vm/console.php"): run_command("/home/xc_vm/bin/php/bin/php /home/xc_vm/console.php status 1") # Set config permissions run_command("chown -R xc_vm:xc_vm /home/xc_vm/config/") # Run startup command via console.php startup_cmd = "/home/xc_vm/console.php" if os.path.exists(startup_cmd): run_command( f"/home/xc_vm/bin/php/bin/php {startup_cmd} startup >/dev/null 2>&1" ) # Download GeoLite2 GeoIP databases (City/Country/ASN). They are no longer # bundled in the repo/release archive, so fetch them from the XC_VM_Update # release on install. This also refreshes bin/maxmind/version.json. # Non-fatal: run_command never raises, so a download failure won't abort. if os.path.exists(startup_cmd): printc("Downloading GeoLite2 GeoIP databases...", col.OKBLUE) run_command( f"/home/xc_vm/bin/php/bin/php {startup_cmd} cron:maxmind --force" ) # Download the proxy-node archive (proxy.tar.gz). Like GeoLite2 it is no longer # bundled in the release archive — fetch it from the XC_VM_Proxy release on # install so the first proxy-node install runs from a local copy, and write the # bin/install/proxy_version.json index. Runs as xc_vm (owner of bin/install/); # non-fatal — run_command never raises, so a download failure won't abort install. if os.path.exists(startup_cmd): printc("Downloading proxy node archive...", col.OKBLUE) run_command( f"sudo -u xc_vm /home/xc_vm/bin/php/bin/php {startup_cmd} cron:proxy --force" ) time.sleep(3) # Final restart run_command("service xc_vm restart") # Get server IP server_ip = getIP() ################################################## # FINISHED - SHOW SUMMARY # ################################################## printc("=" * 60, col.OKGREEN) printc("INSTALLATION COMPLETED SUCCESSFULLY!", col.OKGREEN, 1) printc(f"Distribution: {dist_info['id']} {dist_info['version']}", col.OKGREEN) printc(f"Continue Setup: http://{server_ip}:{http_port}/{admin_code}", col.OKBLUE) printc(f"Total RAM: {total_ram_mb}MB", col.OKGREEN) printc("Credentials have been saved to:", col.OKBLUE) printc(f"{local_creds_path}", col.OKGREEN) printc("IMPORTANT: Move the credentials file to a secure location!", col.WARNING) printc("=" * 60, col.OKGREEN)