Files
XC_VM/install
T
Divarion_D 3b163e3d47 fix(install): add libogg0 to debian13/ubuntu20/ubuntu24 package lists
The bundled ffmpeg/ffprobe 8.0 has a hard NEEDED dependency on
libogg.so.0. The debian13, ubuntu20 and ubuntu24 package lists (in
both the MAIN installer and the LB install flow) omitted libogg0,
so ffmpeg would fail to start with "error while loading shared
libraries: libogg.so.0" on those distributions.

Add libogg0 next to libnuma1 in all three lists, in both
install (PACKAGES) and LbInstallFlow::getPackages(), keeping MAIN
and LB in sync. redhat already ships the equivalent libogg RPM.

Refs: #152
2026-08-15 17:31:05 +03:00

2547 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)
print(
"If you want to change the ports, enter new values, or leave empty to use the default ports"
)
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)