mirror of
https://github.com/Vateron-Media/XC_VM.git
synced 2026-09-29 12:02:04 +02:00
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
2547 lines
90 KiB
Python
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)
|