mirror of
https://github.com/myronblair/novacpx
synced 2026-06-30 17:50:41 -05:00
fbc445dad2
Panel no longer depends on the user-managed MariaDB service.
SQLite at /var/lib/novacpx/panel.db runs independently so the
control panel stays up even when MariaDB is stopped.
- DB.php: switch to sqlite: DSN, add SQL translator (ON DUPLICATE KEY,
DATE_ADD/DATE_SUB INTERVAL, NOW(), UNIX_TIMESTAMP(), IFNULL)
- Core.php: replace DB_HOST/NAME/USER/PASS with DB_PATH constant
- schema.sql: full SQLite syntax, add TOTP columns to users table
- _branding.php: use sqlite: PDO, datetime('now') for session check
- install.sh: apt install sqlite3, create SQLite DB instead of MySQL DB
- tools/migrate-to-sqlite.sh: one-shot migration script for existing installs
176 lines
6.7 KiB
Bash
Executable File
176 lines
6.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Migrate NovaCPX panel DB from MySQL to SQLite
|
|
# Run as root on the NovaCPX VM.
|
|
# Usage: bash tools/migrate-to-sqlite.sh [--schema-only]
|
|
|
|
set -euo pipefail
|
|
|
|
SCHEMA_ONLY=false
|
|
[[ "${1:-}" == "--schema-only" ]] && SCHEMA_ONLY=true
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO_ROOT="$(dirname "$SCRIPT_DIR")"
|
|
DB_PATH="/var/lib/novacpx/panel.db"
|
|
SCHEMA="$REPO_ROOT/db/schema.sql"
|
|
CONFIG="/etc/novacpx/config.ini"
|
|
BACKUP_DIR="/var/lib/novacpx/migration-backup-$(date +%Y%m%d-%H%M%S)"
|
|
|
|
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
|
|
log() { echo -e "${GREEN}[✓]${NC} $*"; }
|
|
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
|
|
fail() { echo -e "${RED}[✗]${NC} $*"; exit 1; }
|
|
|
|
[[ $EUID -ne 0 ]] && fail "Run as root."
|
|
command -v sqlite3 >/dev/null 2>&1 || { apt-get install -y sqlite3 >> /dev/null; log "sqlite3 installed"; }
|
|
|
|
# ── Read MySQL creds from existing config ─────────────────────────────────────
|
|
read_ini() { grep -E "^\s*$1\s*=" "$CONFIG" | head -1 | cut -d= -f2- | tr -d ' '; }
|
|
|
|
DB_HOST=$(read_ini host)
|
|
DB_NAME=$(read_ini name)
|
|
DB_USER=$(read_ini user)
|
|
DB_PASS=$(read_ini pass)
|
|
DB_WP_USER=$(read_ini wp_user)
|
|
DB_WP_PASS=$(read_ini wp_pass)
|
|
SECRET_KEY=$(read_ini secret)
|
|
|
|
if [[ -z "$DB_NAME" ]]; then
|
|
warn "No MySQL config found in $CONFIG — this install may already be on SQLite."
|
|
warn "If you only need to (re)create the SQLite DB from schema, use --schema-only."
|
|
[[ "$SCHEMA_ONLY" == "false" ]] && exit 0
|
|
fi
|
|
|
|
# ── Backup ────────────────────────────────────────────────────────────────────
|
|
log "Backing up to $BACKUP_DIR..."
|
|
mkdir -p "$BACKUP_DIR"
|
|
[[ -f "$DB_PATH" ]] && cp "$DB_PATH" "$BACKUP_DIR/panel.db.bak"
|
|
cp "$CONFIG" "$BACKUP_DIR/config.ini.bak"
|
|
[[ -n "$DB_NAME" ]] && mysqldump -u "$DB_USER" -p"$DB_PASS" "$DB_NAME" > "$BACKUP_DIR/mysql-dump.sql" 2>/dev/null && log "MySQL dump saved" || warn "MySQL dump failed (service may be down)"
|
|
|
|
# ── Create SQLite DB from schema ──────────────────────────────────────────────
|
|
log "Creating SQLite database at $DB_PATH..."
|
|
mkdir -p /var/lib/novacpx
|
|
rm -f "$DB_PATH"
|
|
sqlite3 "$DB_PATH" < "$SCHEMA"
|
|
log "Schema applied"
|
|
|
|
if [[ "$SCHEMA_ONLY" == "false" && -n "$DB_NAME" ]]; then
|
|
# ── Migrate data from MySQL → SQLite ─────────────────────────────────────────
|
|
log "Migrating data from MySQL ($DB_NAME)..."
|
|
|
|
TABLES=(
|
|
users settings email_domains email_accounts dns_zones dns_records
|
|
accounts packages features account_features
|
|
ssl_certificates backups backup_schedules
|
|
ftp_accounts cron_jobs databases db_users
|
|
wordpress_installs
|
|
resellers reseller_branding reseller_packages
|
|
ip_addresses ssh_keys firewall_rules
|
|
docker_containers docker_compose_stacks docker_quotas
|
|
webmail_sso_tokens audit_log
|
|
)
|
|
|
|
for TABLE in "${TABLES[@]}"; do
|
|
# Check table exists in MySQL
|
|
ROWS=$(mysql -u "$DB_USER" -p"$DB_PASS" "$DB_NAME" -se "SELECT COUNT(*) FROM \`$TABLE\`;" 2>/dev/null) || { warn "Table $TABLE not found in MySQL — skipping"; continue; }
|
|
[[ "$ROWS" == "0" ]] && { log " $TABLE — empty, skip"; continue; }
|
|
|
|
# Get column names from MySQL for this table
|
|
MYSQL_COLS=$(mysql -u "$DB_USER" -p"$DB_PASS" --skip-column-names --batch "$DB_NAME" \
|
|
-e "SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA='${DB_NAME}' AND TABLE_NAME='${TABLE}' ORDER BY ORDINAL_POSITION;" 2>/dev/null)
|
|
[[ -z "$MYSQL_COLS" ]] && { warn " $TABLE: could not get MySQL columns, skipping"; continue; }
|
|
|
|
# Dump only the columns that exist in both MySQL and SQLite
|
|
mysql -u "$DB_USER" -p"$DB_PASS" --skip-column-names --batch "$DB_NAME" \
|
|
-e "SELECT $(echo "$MYSQL_COLS" | tr '\n' ',' | sed 's/,$//') FROM \`$TABLE\`" 2>/dev/null | \
|
|
python3 - "$TABLE" "$DB_PATH" "$MYSQL_COLS" <<'PYEOF'
|
|
import sys, sqlite3, csv, io
|
|
|
|
table = sys.argv[1]
|
|
db_path = sys.argv[2]
|
|
mysql_cols = [c for c in sys.argv[3].splitlines() if c]
|
|
data = sys.stdin.read()
|
|
if not data.strip():
|
|
sys.exit(0)
|
|
|
|
con = sqlite3.connect(db_path)
|
|
cur = con.cursor()
|
|
cur.execute(f"SELECT name FROM pragma_table_info('{table}')")
|
|
sqlite_cols = {r[0] for r in cur.fetchall()}
|
|
if not sqlite_cols:
|
|
print(f" {table}: not in SQLite schema, skipping", file=sys.stderr)
|
|
con.close()
|
|
sys.exit(0)
|
|
|
|
# Only insert columns present in both
|
|
common = [c for c in mysql_cols if c in sqlite_cols]
|
|
col_idx = [i for i, c in enumerate(mysql_cols) if c in sqlite_cols]
|
|
placeholders = ','.join(['?'] * len(common))
|
|
col_list = ','.join(common)
|
|
|
|
reader = csv.reader(io.StringIO(data), delimiter='\t')
|
|
rows = list(reader)
|
|
inserted = 0
|
|
for row in rows:
|
|
if len(row) != len(mysql_cols):
|
|
continue
|
|
vals = [None if row[i] == 'NULL' else row[i] for i in col_idx]
|
|
try:
|
|
cur.execute(f"INSERT OR IGNORE INTO {table} ({col_list}) VALUES ({placeholders})", vals)
|
|
inserted += 1
|
|
except Exception as e:
|
|
pass
|
|
con.commit()
|
|
con.close()
|
|
print(f" {table}: {inserted}/{len(rows)} rows migrated ({len(common)} cols)")
|
|
PYEOF
|
|
done
|
|
log "Data migration complete"
|
|
fi
|
|
|
|
# ── Update config.ini ─────────────────────────────────────────────────────────
|
|
log "Updating $CONFIG to use SQLite..."
|
|
|
|
# Build new [database] section
|
|
NEW_DB_SECTION="[database]
|
|
path = ${DB_PATH}
|
|
wp_user = ${DB_WP_USER}
|
|
wp_pass = ${DB_WP_PASS}"
|
|
|
|
# Replace everything between [database] and the next [section]
|
|
python3 - "$CONFIG" "$NEW_DB_SECTION" <<'PYEOF'
|
|
import sys, re
|
|
|
|
config_path = sys.argv[1]
|
|
new_section = sys.argv[2]
|
|
|
|
with open(config_path) as f:
|
|
content = f.read()
|
|
|
|
# Replace the [database] section with new content
|
|
content = re.sub(
|
|
r'\[database\].*?(?=\[|\Z)',
|
|
new_section + '\n\n',
|
|
content,
|
|
flags=re.DOTALL
|
|
)
|
|
|
|
with open(config_path, 'w') as f:
|
|
f.write(content)
|
|
|
|
print("config.ini updated")
|
|
PYEOF
|
|
|
|
chown root:www-data "$CONFIG"
|
|
chmod 640 "$CONFIG"
|
|
|
|
# ── Fix permissions ───────────────────────────────────────────────────────────
|
|
chown www-data:www-data "$DB_PATH"
|
|
chmod 660 "$DB_PATH"
|
|
|
|
log "Migration complete!"
|
|
log "Panel DB: $DB_PATH"
|
|
log "Backup: $BACKUP_DIR"
|
|
warn "Restart PHP-FPM to clear any cached DB connections:"
|
|
echo " systemctl restart php8.3-fpm php8.2-fpm php8.1-fpm php7.4-fpm 2>/dev/null || true"
|