2025-07-10 20:01:56 +03:00
#!/usr/bin/python3
2026-04-13 21:52:20 +03:00
import hashlib
2026-04-07 19:55:28 +03:00
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
2025-07-10 20:01:56 +03:00
if sys . version_info . major != 3 :
print ( "Please run with python3." )
sys . exit ( 1 )
rPath = os . path . dirname ( os . path . realpath ( __file__ ))
2026-04-07 19:55:28 +03:00
# 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" ,
2026-04-14 18:14:02 +03:00
"libsodium23" ,
2026-04-07 19:55:28 +03:00
"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" ,
2026-04-14 18:14:02 +03:00
"libsodium23" ,
2026-04-07 19:55:28 +03:00
"cpufrequtils" ,
"mcrypt" ,
2026-08-15 18:29:56 +03:00
"libogg0" ,
2026-04-07 19:55:28 +03:00
"libnuma1" ,
],
"ubuntu20" : [
"iproute2" ,
"net-tools" ,
"dirmngr" ,
"gpg-agent" ,
"software-properties-common" ,
"wget" ,
"curl" ,
"unzip" ,
"zip" ,
"xz-utils" ,
"cron" ,
"git" ,
"sysstat" ,
"ca-certificates" ,
2026-06-12 15:32:22 +03:00
"libcurl4-gnutls-dev" ,
2026-04-07 19:55:28 +03:00
"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" ,
2026-04-14 18:14:02 +03:00
"libsodium23" ,
2026-04-07 19:55:28 +03:00
"cpufrequtils" ,
"mcrypt" ,
2026-08-15 18:29:56 +03:00
"libogg0" ,
2026-04-07 19:55:28 +03:00
"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" ,
2026-04-14 18:14:02 +03:00
"libsodium23" ,
2026-04-07 19:55:28 +03:00
"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" ,
2026-04-14 18:14:02 +03:00
"libpng-dev" ,
2026-04-07 19:55:28 +03:00
"libharfbuzz-dev" ,
"libfribidi-dev" ,
"libgeoip1t64" ,
"geoip-bin" ,
2026-04-14 18:14:02 +03:00
"libsodium23" ,
2026-04-07 19:55:28 +03:00
"cpufrequtils" ,
"mcrypt" ,
2026-08-15 18:29:56 +03:00
"libogg0" ,
2026-04-07 19:55:28 +03:00
"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" ,
2026-04-14 18:14:02 +03:00
"libsodium23" ,
2026-04-07 19:55:28 +03:00
"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" ,
2026-04-14 18:14:02 +03:00
"libsodium" ,
2026-04-07 19:55:28 +03:00
"numactl" ,
"kernel-tools" ,
],
}
2025-07-10 20:01:56 +03:00
rRemove = [ "mysql-server" ]
2026-04-07 19:55:28 +03:00
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
2026-04-21 20:48:05 +03:00
thread_pool_max_threads = {{THREAD_POOL_MAX_THREADS}}
2026-04-07 19:55:28 +03:00
[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
2026-06-19 21:58:38 +03:00
TimeoutStopSec=30
KillMode=mixed
2026-04-07 19:55:28 +03:00
ExecStart=/bin/bash /home/xc_vm/service start
ExecStop=/bin/bash /home/xc_vm/service stop
2026-06-19 21:58:38 +03:00
ExecReload=/bin/bash /home/xc_vm/service restart
2026-04-07 19:55:28 +03:00
[Install]
WantedBy=multi-user.target"""
2025-07-10 20:01:56 +03:00
rChoice = "23456789abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ"
rConfigPath = "/home/xc_vm/config/config.ini"
2026-04-07 19:55:28 +03:00
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 )
2026-04-13 21:52:20 +03:00
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
2026-04-07 19:55:28 +03:00
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
2026-05-14 22:05:10 +03:00
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- %d T%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
2026-04-07 19:55:28 +03:00
# 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 )
2026-06-26 18:12:11 +03:00
# 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
2026-04-07 19:55:28 +03:00
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
2026-04-13 21:52:20 +03:00
# 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 )
2026-04-07 19:55:28 +03:00
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 )
2026-05-14 22:05:10 +03:00
write_bin_version_file ( release_tag , patch_name , dist_id , version )
2026-04-07 19:55:28 +03:00
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 )
2026-06-25 21:21:43 +03:00
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 )
2026-04-07 19:55:28 +03:00
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 )
2026-04-13 21:52:20 +03:00
# 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 )
2026-04-07 19:55:28 +03:00
# 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
)
2026-04-14 18:14:02 +03:00
ret , _ , _ = run_command (
"curl -LsS https://r.mariadb.com/downloads/mariadb_repo_setup | bash -s -- --mariadb-server-version='mariadb-11.4'" ,
capture_output = True ,
2026-04-07 19:55:28 +03:00
)
2026-04-14 18:14:02 +03:00
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"
)
2026-04-07 19:55:28 +03:00
# 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 ""
2026-04-21 20:48:05 +03:00
is_mariadb_103 = ( "10.3" in mariadb_version or "5.7" in mariadb_version or "5.6" in mariadb_version )
2026-04-07 19:55:28 +03:00
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
2025-07-10 20:01:56 +03:00
2025-12-14 22:24:09 +03:00
def get_system_ram_mb ():
"""Get total system RAM in MB"""
try :
2026-04-07 19:55:28 +03:00
with open ( "/proc/meminfo" , "r" ) as f :
2025-12-14 22:24:09 +03:00
for line in f :
2026-04-07 19:55:28 +03:00
if line . startswith ( "MemTotal:" ):
2025-12-14 22:24:09 +03:00
mem_kb = int ( line . split ()[ 1 ])
return mem_kb // 1024 # Convert to MB
2026-04-07 19:55:28 +03:00
except Exception :
2025-12-14 22:24:09 +03:00
pass
return 1024 # Value by default if unable to determine
def generate_mysql_config ( total_ram_mb ):
2026-04-07 19:55:28 +03:00
"""Generate MySQL configuration based on total RAM (from bash script logic)"""
2025-12-14 22:24:09 +03:00
2026-04-07 19:55:28 +03:00
# Calculate based on RAM (similar to bash script)
2025-12-14 22:24:09 +03:00
buffer_pool_mb = int ( total_ram_mb * 0.25 )
if total_ram_mb < 512 :
buffer_pool_mb = 64
2026-04-21 20:48:05 +03:00
max_connections = 40
2025-12-14 22:24:09 +03:00
elif total_ram_mb < 1024 :
2026-04-07 19:55:28 +03:00
if buffer_pool_mb > 128 :
buffer_pool_mb = 128
2026-04-21 20:48:05 +03:00
max_connections = 80
2025-12-14 22:24:09 +03:00
elif total_ram_mb < 2048 :
2026-04-07 19:55:28 +03:00
if buffer_pool_mb > 256 :
buffer_pool_mb = 256
2026-04-21 20:48:05 +03:00
max_connections = 120
2025-12-14 22:24:09 +03:00
elif total_ram_mb < 4096 :
2026-04-07 19:55:28 +03:00
if buffer_pool_mb > 512 :
buffer_pool_mb = 512
2026-04-21 20:48:05 +03:00
max_connections = 200
2025-12-14 22:24:09 +03:00
elif total_ram_mb < 8192 :
2026-04-07 19:55:28 +03:00
if buffer_pool_mb > 1024 :
buffer_pool_mb = 1024
2026-04-21 20:48:05 +03:00
max_connections = 300
elif total_ram_mb < 16384 :
2026-04-07 19:55:28 +03:00
if buffer_pool_mb > 2048 :
buffer_pool_mb = 2048
2026-04-21 20:48:05 +03:00
max_connections = 450
else :
if buffer_pool_mb > 4096 :
buffer_pool_mb = 4096
max_connections = 600
2025-12-14 22:24:09 +03:00
2026-04-07 19:55:28 +03:00
# 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"
2025-12-14 22:24:09 +03:00
2026-04-07 19:55:28 +03:00
# Calculate other values
2025-12-14 22:24:09 +03:00
key_buffer = min ( buffer_pool_mb // 8 , 32 )
tmp_table_size = min ( buffer_pool_mb // 4 , 64 )
2026-04-21 20:48:05 +03:00
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 )
2026-04-07 19:55:28 +03:00
buffer_pool_instances = "1" if buffer_pool_mb < 1024 else "2"
2025-12-14 22:24:09 +03:00
2026-04-07 19:55:28 +03:00
# Generate config from template
2025-12-14 22:24:09 +03:00
config = rMySQLCnfTemplate
config = config . replace ( "{{KEY_BUFFER}}" , str ( key_buffer ))
config = config . replace ( "{{MAX_CONNECTIONS}}" , str ( max_connections ))
2026-04-07 19:55:28 +03:00
config = config . replace ( "{{BACK_LOG}}" , str ( back_log ))
2025-12-14 22:24:09 +03:00
config = config . replace ( "{{TMP_TABLE_SIZE}}" , str ( tmp_table_size ))
config = config . replace ( "{{BUFFER_POOL_SIZE}}" , buffer_pool_size )
2026-04-07 19:55:28 +03:00
config = config . replace ( "{{BUFFER_POOL_INSTANCES}}" , buffer_pool_instances )
config = config . replace ( "{{THREAD_CACHE}}" , str ( thread_cache ))
2026-04-21 20:48:05 +03:00
config = config . replace ( "{{THREAD_POOL_MAX_THREADS}}" , str ( thread_pool_max_threads ))
2025-12-14 22:24:09 +03:00
printc ( f "RAM detected: { total_ram_mb } MB" , col . OKGREEN )
2026-04-07 19:55:28 +03:00
printc (
f "Buffer pool configured: { buffer_pool_size } ( { buffer_pool_mb } MB)" , col . OKGREEN
)
2025-12-14 22:24:09 +03:00
printc ( f "Max connections: { max_connections } " , col . OKGREEN )
2026-04-21 20:48:05 +03:00
printc ( f "Thread pool max threads: { thread_pool_max_threads } " , col . OKGREEN )
2025-12-14 22:24:09 +03:00
return config
2026-04-07 19:55:28 +03:00
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
2026-04-14 18:14:02 +03:00
chars = "23456789abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ"
2026-04-07 19:55:28 +03:00
return "" . join ( random . choice ( chars ) for _ in range ( 20 ))
2025-07-10 20:01:56 +03:00
2026-04-07 19:55:28 +03:00
def sanitize_password_for_sql ( password ):
"""Escape single quotes for safe use in SQL strings"""
return password . replace ( "'" , "''" )
2025-07-10 20:01:56 +03:00
2026-04-07 19:55:28 +03:00
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
2025-07-10 20:01:56 +03:00
def getIP ():
2026-04-07 19:55:28 +03:00
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"
2025-07-10 20:01:56 +03:00
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 ( " " )
2026-04-07 19:55:28 +03:00
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
2026-04-14 18:14:02 +03:00
# 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
2026-04-07 19:55:28 +03:00
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
2026-04-14 18:14:02 +03:00
# Update dynamic linker cache
run_command ( "ldconfig 2>/dev/null || true" )
2026-04-07 19:55:28 +03:00
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 )
2026-07-02 21:28:52 +03:00
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
2026-04-07 19:55:28 +03:00
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 :
2026-04-14 18:14:02 +03:00
# 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 ]
2026-07-02 21:28:52 +03:00
# Install non-DB packages one by one: a frame per package, log shown below.
2026-04-14 18:14:02 +03:00
if other_packages :
printc (
f "Installing { label } system packages ( { len ( other_packages ) } )..." ,
col . OKBLUE ,
)
2026-07-02 21:28:52 +03:00
failed = _apt_install_framed ( other_packages )
if failed :
2026-04-14 18:14:02 +03:00
printc (
2026-07-02 21:28:52 +03:00
"These packages failed to install: " + ", " . join ( failed ),
2026-04-14 18:14:02 +03:00
col . WARNING ,
)
# Install MariaDB packages separately with fallback
if db_packages :
db_str = " " . join ( db_packages )
printc (
2026-07-02 21:28:52 +03:00
f "Installing MariaDB packages: { db_str } " , col . OKBLUE
2026-04-14 18:14:02 +03:00
)
2026-07-02 21:28:52 +03:00
# 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 } "
2026-04-14 18:14:02 +03:00
)
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 ,
)
2026-04-07 19:55:28 +03:00
# 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" )
2025-07-10 20:01:56 +03:00
if __name__ == "__main__" :
##################################################
# START #
##################################################
2026-04-07 19:55:28 +03:00
printc ( "XC_VM Multi-Distribution Installer" , col . OKGREEN , 2 )
2025-07-10 20:01:56 +03:00
2026-04-07 19:55:28 +03:00
# 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 ,
2025-07-10 20:01:56 +03:00
)
sys . exit ( 1 )
rHost = "127.0.0.1"
rServerID = 1
2026-04-07 19:55:28 +03:00
rUsername = generate_random_password ( 32 ) # Username de 32 caracteres
rPassword = generate_random_password ( 32 ) # Password de 32 caracteres
2025-07-10 20:01:56 +03:00
rDatabase = "xc_vm"
rPort = 3306
2026-04-07 19:55:28 +03:00
# 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 )
2025-07-10 20:01:56 +03:00
if os . path . exists ( "/home/xc_vm/" ):
2026-04-07 19:55:28 +03:00
printc ( "XC_VM Directory Exists!" , col . WARNING )
2025-07-10 20:01:56 +03:00
while True :
2026-04-07 19:55:28 +03:00
rAnswer = input ( "Continue and overwrite? (Y / N) : " ) . strip () . upper ()
if rAnswer in [ "Y" , "N" ]:
2025-07-10 20:01:56 +03:00
break
if rAnswer == "N" :
sys . exit ( 1 )
##################################################
2026-04-07 19:55:28 +03:00
# SYSTEM PREPARATION #
2025-07-10 20:01:56 +03:00
##################################################
2026-04-07 19:55:28 +03:00
printc ( "Preparing System" , col . OKBLUE )
2025-07-10 20:01:56 +03:00
2026-04-07 19:55:28 +03:00
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
2025-07-10 20:01:56 +03:00
2026-04-07 19:55:28 +03:00
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 )
2026-04-14 18:14:02 +03:00
# 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" )
2026-04-07 19:55:28 +03:00
# 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 ()
2025-07-10 20:01:56 +03:00
else :
2026-04-07 19:55:28 +03:00
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 )
2025-07-10 20:01:56 +03:00
try :
2026-04-07 19:55:28 +03:00
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 )
2026-07-02 21:28:52 +03:00
# capture_output=True keeps adduser/useradd's raw "info:" lines out of the
# terminal so the framed printc messages stay clean and consistent.
2026-04-07 19:55:28 +03:00
if dist_info [ "family" ] == "debian" :
2026-07-02 21:28:52 +03:00
rc , _ , err = run_command (
"adduser --system --shell /bin/false --no-create-home --home /nonexistent --group --disabled-login xc_vm" ,
capture_output = True ,
2026-04-07 19:55:28 +03:00
)
else : # redhat
2026-07-02 21:28:52 +03:00
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 )
2026-04-07 19:55:28 +03:00
2025-07-10 20:01:56 +03:00
if not os . path . exists ( "/home/xc_vm" ):
2026-04-07 19:55:28 +03:00
os . makedirs ( "/home/xc_vm" , exist_ok = True )
run_command ( "chown xc_vm:xc_vm /home/xc_vm" )
2025-07-10 20:01:56 +03:00
##################################################
2026-04-07 19:55:28 +03:00
# INSTALL XC_VM #
2025-07-10 20:01:56 +03:00
##################################################
2026-04-07 19:55:28 +03:00
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 )
2026-06-25 21:21:43 +03:00
# Replace the shipped placeholder cert with a unique per-install one
# (before nginx is started further below).
generate_self_signed_cert ()
2026-04-07 19:55:28 +03:00
# 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 (
2026-06-26 18:12:11 +03:00
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 ,
2026-04-07 19:55:28 +03:00
)
2026-06-26 18:12:11 +03:00
sys . exit ( 1 )
2026-04-07 19:55:28 +03:00
# 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 (
2026-06-26 18:12:11 +03:00
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 ,
2026-04-07 19:55:28 +03:00
)
2026-06-26 18:12:11 +03:00
sys . exit ( 1 )
2026-04-07 19:55:28 +03:00
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 (
2026-06-26 18:12:11 +03:00
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 ,
2026-04-07 19:55:28 +03:00
)
2026-06-26 18:12:11 +03:00
sys . exit ( 1 )
2026-04-07 19:55:28 +03:00
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 ,
)
2025-07-10 20:01:56 +03:00
##################################################
2026-04-07 19:55:28 +03:00
# MariaDB CONFIGURATION #
2025-07-10 20:01:56 +03:00
##################################################
2026-04-07 19:55:28 +03:00
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
2025-12-14 22:24:09 +03:00
total_ram_mb = get_system_ram_mb ()
rMySQLCnf = generate_mysql_config ( total_ram_mb )
2026-04-07 19:55:28 +03:00
# 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 :
2025-07-10 20:01:56 +03:00
rFile . write ( rMySQLCnf )
2026-04-07 19:55:28 +03:00
# 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;"'
2025-07-10 20:01:56 +03:00
)
2026-04-07 19:55:28 +03:00
# 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 )
2025-07-10 20:01:56 +03:00
rConfigData = rConfig % ( rUsername , rPassword )
2026-04-07 19:55:28 +03:00
with io . open ( rConfigPath , "w" , encoding = "utf-8" ) as rFile :
rFile . write ( rConfigData )
2026-09-16 17:43:53 +03:00
# Per-install OPENSSL_EXTRA secret — key material for Encryption (hmac_keys,
# cached image filenames, proxy URL keys, stream tokens). Generated ONCE and
# never rotated: rotating it would make all existing encrypted data
# undecryptable, so only create the file when absent (re-running the installer
# must never overwrite it). PHP reads it via ConstantsInitializer, falling
# back to the historical literal when the file is missing.
rOpensslExtraPath = "/home/xc_vm/config/openssl_extra"
if not os . path . exists ( rOpensslExtraPath ):
with io . open ( rOpensslExtraPath , "w" , encoding = "utf-8" ) as rFile :
rFile . write ( generate_random_password ( 40 ))
os . chmod ( rOpensslExtraPath , 0o600 )
2026-04-07 19:55:28 +03:00
printc ( "MariaDB configuration completed" , col . OKGREEN )
2025-07-10 20:01:56 +03:00
##################################################
2026-04-07 19:55:28 +03:00
# SYSTEM CONFIGURATION #
2025-07-10 20:01:56 +03:00
##################################################
2026-04-07 19:55:28 +03:00
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 (
" \n tmpfs /home/xc_vm/content/streams tmpfs defaults,noatime,nosuid,nodev,noexec,mode=1777,size=90% 0 0 \n tmpfs /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 )
2026-08-23 13:49:48 +03:00
printc ( "Enter new port values, or leave empty for defaults" , col . OKBLUE )
2025-10-03 19:49:43 +03:00
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
2026-04-07 19:55:28 +03:00
printc ( "Error: port must be a number between 1 and 65535" , col . FAIL )
2025-10-03 19:49:43 +03:00
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
2026-04-07 19:55:28 +03:00
printc ( "Error: port must be a number between 1 and 65535" , col . FAIL )
2025-10-03 19:49:43 +03:00
# Write HTTP ports configuration
http_conf_path = "/home/xc_vm/bin/nginx/conf/ports/http.conf"
2026-04-07 19:55:28 +03:00
os . makedirs ( os . path . dirname ( http_conf_path ), exist_ok = True )
2025-10-03 19:49:43 +03:00
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"
2026-04-07 19:55:28 +03:00
os . makedirs ( os . path . dirname ( https_conf_path ), exist_ok = True )
2025-10-03 19:49:43 +03:00
with io . open ( https_conf_path , "w" , encoding = "utf-8" ) as rFile :
rFile . write ( f "listen { https_port } ssl;" )
2026-04-07 19:55:28 +03:00
printc ( f "Ports configured: HTTP - { http_port } , HTTPS - { https_port } " , col . OKGREEN )
2025-10-03 19:49:43 +03:00
2026-04-21 21:48:10 +03:00
# 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 )
2026-04-07 19:55:28 +03:00
# Configure sysctl
2025-07-10 20:01:56 +03:00
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
2026-04-07 19:55:28 +03:00
2025-07-10 20:01:56 +03:00
if rAnswer . upper () == "Y" :
try :
2026-04-07 19:55:28 +03:00
run_command ( "modprobe ip_conntrack 2>/dev/null || true" )
except Exception :
2025-07-10 20:01:56 +03:00
pass
try :
2026-04-07 19:55:28 +03:00
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 )
2025-07-10 20:01:56 +03:00
else :
if os . path . exists ( "/home/xc_vm/config/sysctl.on" ):
os . remove ( "/home/xc_vm/config/sysctl.on" )
2026-04-07 19:55:28 +03:00
# 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 ( " \n DefaultLimitNOFILE=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 ( " \n DefaultLimitNOFILE=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" )
2025-07-10 20:01:56 +03:00
##################################################
# ACCESS CODE #
##################################################
2026-04-07 19:55:28 +03:00
printc ( "Generating access code" , col . OKBLUE )
2025-07-10 20:01:56 +03:00
rCodeDir = "/home/xc_vm/bin/nginx/conf/codes/"
2026-04-07 19:55:28 +03:00
# 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 )
2025-07-10 20:01:56 +03:00
else :
2026-04-07 19:55:28 +03:00
printc ( f "Using existing access code: { admin_code } " , col . OKGREEN )
2025-07-10 20:01:56 +03:00
##################################################
2026-04-07 19:55:28 +03:00
# FINAL CONFIGURATION #
2025-07-10 20:01:56 +03:00
##################################################
2026-04-07 19:55:28 +03:00
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" )
2025-07-10 20:01:56 +03:00
2026-04-07 19:55:28 +03:00
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 )
2025-07-10 20:01:56 +03:00
time . sleep ( 10 )
2026-07-04 19:04:19 +03:00
# Run status command (root by design: root crontab, system limits, DB
# migrations — the command refuses to run as any other user).
2026-04-07 19:55:28 +03:00
if os . path . exists ( "/home/xc_vm/console.php" ):
2026-07-04 19:04:19 +03:00
run_command ( "/home/xc_vm/bin/php/bin/php /home/xc_vm/console.php status 1" )
2026-04-07 19:55:28 +03:00
# 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 (
2026-07-04 19:04:19 +03:00
f "/home/xc_vm/bin/php/bin/php { startup_cmd } startup >/dev/null 2>&1"
2026-04-07 19:55:28 +03:00
)
2025-07-10 20:01:56 +03:00
2026-06-22 10:35:14 +03:00
# 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 (
2026-07-04 19:04:19 +03:00
f "/home/xc_vm/bin/php/bin/php { startup_cmd } cron:maxmind --force"
2026-06-22 10:35:14 +03:00
)
2026-07-08 19:49:10 +03:00
# 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"
)
2025-07-10 20:01:56 +03:00
time . sleep ( 3 )
2026-04-07 19:55:28 +03:00
# Final restart
run_command ( "service xc_vm restart" )
2025-12-14 22:24:09 +03:00
2026-04-07 19:55:28 +03:00
# 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 )