Fix backup pg_dump resolution across environments
This commit is contained in:
parent
5ee962fdb3
commit
2cef28ff3b
@ -8,6 +8,7 @@ import logging
|
|||||||
import hashlib
|
import hashlib
|
||||||
import tarfile
|
import tarfile
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import shutil
|
||||||
import fcntl
|
import fcntl
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
@ -51,6 +52,29 @@ class BackupService:
|
|||||||
self.files_dir = self.backup_dir / "files"
|
self.files_dir = self.backup_dir / "files"
|
||||||
self.db_dir.mkdir(exist_ok=True)
|
self.db_dir.mkdir(exist_ok=True)
|
||||||
self.files_dir.mkdir(exist_ok=True)
|
self.files_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
def _resolve_pg_binary(self, binary_name: str) -> str:
|
||||||
|
"""Resolve PostgreSQL CLI binaries across container/host environments."""
|
||||||
|
found = shutil.which(binary_name)
|
||||||
|
if found:
|
||||||
|
return found
|
||||||
|
|
||||||
|
candidates = [
|
||||||
|
f"/usr/bin/{binary_name}",
|
||||||
|
f"/usr/local/bin/{binary_name}",
|
||||||
|
f"/opt/homebrew/bin/{binary_name}",
|
||||||
|
f"/usr/lib/postgresql/16/bin/{binary_name}",
|
||||||
|
f"/usr/lib/postgresql/15/bin/{binary_name}",
|
||||||
|
f"/usr/lib/postgresql/14/bin/{binary_name}",
|
||||||
|
]
|
||||||
|
|
||||||
|
for candidate in candidates:
|
||||||
|
if Path(candidate).exists():
|
||||||
|
return candidate
|
||||||
|
|
||||||
|
raise FileNotFoundError(
|
||||||
|
f"{binary_name} blev ikke fundet i PATH. Installer postgresql-client eller rebuild API image."
|
||||||
|
)
|
||||||
|
|
||||||
async def create_database_backup(self, is_monthly: bool = False) -> Optional[int]:
|
async def create_database_backup(self, is_monthly: bool = False) -> Optional[int]:
|
||||||
"""
|
"""
|
||||||
@ -83,6 +107,8 @@ class BackupService:
|
|||||||
job_id, backup_format, is_monthly)
|
job_id, backup_format, is_monthly)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
pg_dump_bin = self._resolve_pg_binary('pg_dump')
|
||||||
|
|
||||||
# Build pg_dump command - connect via network to postgres service
|
# Build pg_dump command - connect via network to postgres service
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
env['PGPASSWORD'] = settings.DATABASE_URL.split(':')[2].split('@')[0] # Extract password
|
env['PGPASSWORD'] = settings.DATABASE_URL.split(':')[2].split('@')[0] # Extract password
|
||||||
@ -102,10 +128,10 @@ class BackupService:
|
|||||||
|
|
||||||
if backup_format == 'dump':
|
if backup_format == 'dump':
|
||||||
# Compressed custom format (-Fc)
|
# Compressed custom format (-Fc)
|
||||||
cmd = ['pg_dump', '-h', host, '-U', user, '-Fc', dbname]
|
cmd = [pg_dump_bin, '-h', host, '-U', user, '-Fc', dbname]
|
||||||
else:
|
else:
|
||||||
# Plain SQL format
|
# Plain SQL format
|
||||||
cmd = ['pg_dump', '-h', host, '-U', user, dbname]
|
cmd = [pg_dump_bin, '-h', host, '-U', user, dbname]
|
||||||
|
|
||||||
# Execute pg_dump and write to file
|
# Execute pg_dump and write to file
|
||||||
logger.info("📦 Executing: %s > %s", ' '.join(cmd), backup_path)
|
logger.info("📦 Executing: %s > %s", ' '.join(cmd), backup_path)
|
||||||
@ -154,6 +180,21 @@ class BackupService:
|
|||||||
backup_path.unlink()
|
backup_path.unlink()
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
error_msg = str(e)
|
||||||
|
logger.error("❌ Database backup failed: %s", error_msg)
|
||||||
|
|
||||||
|
execute_update(
|
||||||
|
"""UPDATE backup_jobs
|
||||||
|
SET status = %s, completed_at = %s, error_message = %s
|
||||||
|
WHERE id = %s""",
|
||||||
|
('failed', datetime.now(), error_msg, job_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
if backup_path.exists():
|
||||||
|
backup_path.unlink()
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
async def create_files_backup(self) -> Optional[int]:
|
async def create_files_backup(self) -> Optional[int]:
|
||||||
"""
|
"""
|
||||||
@ -411,10 +452,13 @@ class BackupService:
|
|||||||
dbname = host_db[1] if len(host_db) > 1 else 'bmc_hub'
|
dbname = host_db[1] if len(host_db) > 1 else 'bmc_hub'
|
||||||
|
|
||||||
env['PGPASSWORD'] = password
|
env['PGPASSWORD'] = password
|
||||||
|
|
||||||
|
psql_bin = self._resolve_pg_binary('psql')
|
||||||
|
pg_restore_bin = self._resolve_pg_binary('pg_restore')
|
||||||
|
|
||||||
# Step 1: Create new empty database
|
# Step 1: Create new empty database
|
||||||
logger.info("📦 Creating new database: %s", new_dbname)
|
logger.info("📦 Creating new database: %s", new_dbname)
|
||||||
create_cmd = ['psql', '-h', host, '-U', user, '-d', 'postgres', '-c',
|
create_cmd = [psql_bin, '-h', host, '-U', user, '-d', 'postgres', '-c',
|
||||||
f"CREATE DATABASE {new_dbname} OWNER {user};"]
|
f"CREATE DATABASE {new_dbname} OWNER {user};"]
|
||||||
result = subprocess.run(create_cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE,
|
result = subprocess.run(create_cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE,
|
||||||
text=True, env=env)
|
text=True, env=env)
|
||||||
@ -430,7 +474,7 @@ class BackupService:
|
|||||||
# Build restore command based on format
|
# Build restore command based on format
|
||||||
if backup['backup_format'] == 'dump':
|
if backup['backup_format'] == 'dump':
|
||||||
# Restore from compressed custom format
|
# Restore from compressed custom format
|
||||||
cmd = ['pg_restore', '-h', host, '-U', user, '-d', new_dbname]
|
cmd = [pg_restore_bin, '-h', host, '-U', user, '-d', new_dbname]
|
||||||
|
|
||||||
logger.info("📥 Restoring to %s: %s < %s", new_dbname, ' '.join(cmd), backup_path)
|
logger.info("📥 Restoring to %s: %s < %s", new_dbname, ' '.join(cmd), backup_path)
|
||||||
|
|
||||||
@ -461,7 +505,7 @@ class BackupService:
|
|||||||
if has_real_errors and not is_harmless:
|
if has_real_errors and not is_harmless:
|
||||||
logger.error("❌ pg_restore had REAL errors: %s", result.stderr[:1000])
|
logger.error("❌ pg_restore had REAL errors: %s", result.stderr[:1000])
|
||||||
# Try to drop the failed database
|
# Try to drop the failed database
|
||||||
subprocess.run(['psql', '-h', host, '-U', user, '-d', 'postgres', '-c',
|
subprocess.run([psql_bin, '-h', host, '-U', user, '-d', 'postgres', '-c',
|
||||||
f"DROP DATABASE IF EXISTS {new_dbname};"], env=env)
|
f"DROP DATABASE IF EXISTS {new_dbname};"], env=env)
|
||||||
raise RuntimeError(f"pg_restore failed with errors")
|
raise RuntimeError(f"pg_restore failed with errors")
|
||||||
else:
|
else:
|
||||||
@ -469,7 +513,7 @@ class BackupService:
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
# Restore from plain SQL
|
# Restore from plain SQL
|
||||||
cmd = ['psql', '-h', host, '-U', user, '-d', new_dbname]
|
cmd = [psql_bin, '-h', host, '-U', user, '-d', new_dbname]
|
||||||
|
|
||||||
logger.info("📥 Executing: %s < %s", ' '.join(cmd), backup_path)
|
logger.info("📥 Executing: %s < %s", ' '.join(cmd), backup_path)
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user