Files
XC_VM/install
T
Divarion_D 07a80da0fa feat(install): frame the port-configuration prompt
Wrap the port-config instruction line in the printc box, matching the framed sections the installer already prints.
2026-08-23 13:49:48 +03:00

2545 lines
90 KiB
Python

#!/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)
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)