289 lines
9.7 KiB
Python
289 lines
9.7 KiB
Python
"""
|
|
Backup Scheduler Module
|
|
Handles automatic backup scheduling and execution
|
|
"""
|
|
|
|
from apscheduler.schedulers.background import BackgroundScheduler
|
|
from apscheduler.triggers.cron import CronTrigger
|
|
import logging
|
|
import os
|
|
import subprocess
|
|
from datetime import datetime, timedelta
|
|
import pymysql
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Database configuration
|
|
DB_HOST = os.getenv('DB_HOST', 'mariadb')
|
|
DB_PORT = int(os.getenv('DB_PORT', '3306'))
|
|
DB_USER = os.getenv('DB_USER', 'quality_user')
|
|
DB_PASSWORD = os.getenv('DB_PASSWORD', 'quality_pass')
|
|
DB_NAME = os.getenv('DB_NAME', 'quality_db')
|
|
|
|
scheduler = None
|
|
|
|
|
|
def get_db():
|
|
"""Get database connection"""
|
|
try:
|
|
conn = pymysql.connect(
|
|
host=DB_HOST,
|
|
port=DB_PORT,
|
|
user=DB_USER,
|
|
password=DB_PASSWORD,
|
|
database=DB_NAME
|
|
)
|
|
return conn
|
|
except Exception as e:
|
|
logger.error(f"Database connection error: {e}")
|
|
return None
|
|
|
|
|
|
def execute_backup(schedule_id, schedule_name, backup_type='full'):
|
|
"""Execute a backup for a schedule"""
|
|
try:
|
|
logger.info(f"Executing scheduled backup: {schedule_name} (Type: {backup_type})")
|
|
|
|
backups_dir = '/app/data/backups'
|
|
if not os.path.exists(backups_dir):
|
|
os.makedirs(backups_dir)
|
|
|
|
# Create backup filename with timestamp
|
|
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
if backup_type == 'data_only':
|
|
filename = f'backup_data_{timestamp}.sql'
|
|
dump_cmd = f'mysqldump -h {DB_HOST} -u {DB_USER} -p{DB_PASSWORD} --skip-ssl --no-create-info {DB_NAME}'
|
|
else:
|
|
filename = f'backup_full_{timestamp}.sql'
|
|
dump_cmd = f'mysqldump -h {DB_HOST} -u {DB_USER} -p{DB_PASSWORD} --skip-ssl {DB_NAME}'
|
|
|
|
filepath = os.path.join(backups_dir, filename)
|
|
|
|
# Execute mysqldump
|
|
with open(filepath, 'w') as f:
|
|
result = subprocess.run(dump_cmd, shell=True, stdout=f, stderr=subprocess.PIPE)
|
|
|
|
if result.returncode != 0:
|
|
logger.error(f"Backup failed: {result.stderr.decode()}")
|
|
return False
|
|
|
|
# Update schedule last_run and next_run
|
|
conn = get_db()
|
|
if conn:
|
|
cursor = conn.cursor()
|
|
|
|
# Get schedule details
|
|
cursor.execute("""
|
|
SELECT frequency, day_of_week, time_of_day
|
|
FROM backup_schedules
|
|
WHERE id = %s
|
|
""", (schedule_id,))
|
|
result = cursor.fetchone()
|
|
|
|
if result:
|
|
frequency, day_of_week, time_of_day = result
|
|
|
|
# Calculate next run
|
|
now = datetime.now()
|
|
time_parts = str(time_of_day).split(':')
|
|
hour = int(time_parts[0])
|
|
minute = int(time_parts[1])
|
|
|
|
if frequency == 'daily':
|
|
next_run = now.replace(hour=hour, minute=minute, second=0, microsecond=0)
|
|
if next_run <= now:
|
|
next_run += timedelta(days=1)
|
|
else: # weekly
|
|
days_of_week = {
|
|
'Monday': 0, 'Tuesday': 1, 'Wednesday': 2, 'Thursday': 3,
|
|
'Friday': 4, 'Saturday': 5, 'Sunday': 6
|
|
}
|
|
target_day = days_of_week.get(day_of_week, 0)
|
|
current_day = now.weekday()
|
|
days_ahead = (target_day - current_day) % 7
|
|
if days_ahead == 0:
|
|
next_run = now.replace(hour=hour, minute=minute, second=0, microsecond=0)
|
|
if next_run <= now:
|
|
days_ahead = 7
|
|
else:
|
|
next_run = now + timedelta(days=days_ahead)
|
|
else:
|
|
next_run = now + timedelta(days=days_ahead)
|
|
next_run = next_run.replace(hour=hour, minute=minute, second=0, microsecond=0)
|
|
|
|
# Update schedule
|
|
cursor.execute("""
|
|
UPDATE backup_schedules
|
|
SET last_run = NOW(), next_run = %s
|
|
WHERE id = %s
|
|
""", (next_run, schedule_id))
|
|
|
|
conn.commit()
|
|
|
|
cursor.close()
|
|
conn.close()
|
|
|
|
# Check retention policy and delete old backups
|
|
cleanup_old_backups()
|
|
|
|
logger.info(f"Backup completed successfully: {filename}")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error executing backup: {e}")
|
|
return False
|
|
|
|
|
|
def cleanup_old_backups():
|
|
"""Clean up old backups based on retention policy"""
|
|
try:
|
|
conn = get_db()
|
|
if not conn:
|
|
return
|
|
|
|
cursor = conn.cursor()
|
|
cursor.execute("""
|
|
SELECT setting_value FROM application_settings
|
|
WHERE setting_key = 'backup_retention_days'
|
|
""")
|
|
result = cursor.fetchone()
|
|
cursor.close()
|
|
|
|
retention_days = int(result[0]) if result else 30
|
|
|
|
# Calculate cutoff date
|
|
cutoff_date = datetime.now() - timedelta(days=retention_days)
|
|
|
|
# Get backups directory
|
|
backups_dir = '/app/data/backups'
|
|
if not os.path.exists(backups_dir):
|
|
conn.close()
|
|
return
|
|
|
|
# Delete old backups
|
|
deleted_count = 0
|
|
for filename in os.listdir(backups_dir):
|
|
filepath = os.path.join(backups_dir, filename)
|
|
if os.path.isfile(filepath):
|
|
file_mtime = datetime.fromtimestamp(os.path.getmtime(filepath))
|
|
if file_mtime < cutoff_date:
|
|
try:
|
|
os.remove(filepath)
|
|
deleted_count += 1
|
|
logger.info(f"Deleted old backup: {filename}")
|
|
except Exception as e:
|
|
logger.warning(f"Failed to delete {filename}: {e}")
|
|
|
|
if deleted_count > 0:
|
|
logger.info(f"Cleaned up {deleted_count} old backups")
|
|
|
|
conn.close()
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error during cleanup: {e}")
|
|
|
|
|
|
def load_schedules():
|
|
"""Load all active schedules from database and register jobs"""
|
|
global scheduler
|
|
|
|
if scheduler is None:
|
|
return
|
|
|
|
try:
|
|
conn = get_db()
|
|
if not conn:
|
|
logger.error("Cannot connect to database for loading schedules")
|
|
return
|
|
|
|
cursor = conn.cursor(pymysql.cursors.DictCursor)
|
|
cursor.execute("""
|
|
SELECT id, schedule_name, frequency, day_of_week, time_of_day, backup_type, is_active
|
|
FROM backup_schedules
|
|
WHERE is_active = 1
|
|
""")
|
|
schedules = cursor.fetchall()
|
|
cursor.close()
|
|
conn.close()
|
|
|
|
# Remove existing jobs for schedules
|
|
for job in scheduler.get_jobs():
|
|
if job.name.startswith('backup_schedule_'):
|
|
scheduler.remove_job(job.id)
|
|
|
|
# Register new jobs
|
|
for schedule in schedules:
|
|
schedule_id = schedule['id']
|
|
schedule_name = schedule['schedule_name']
|
|
frequency = schedule['frequency']
|
|
day_of_week = schedule['day_of_week']
|
|
time_of_day = schedule['time_of_day']
|
|
backup_type = schedule['backup_type']
|
|
|
|
job_id = f"backup_schedule_{schedule_id}"
|
|
|
|
try:
|
|
time_parts = str(time_of_day).split(':')
|
|
hour = int(time_parts[0])
|
|
minute = int(time_parts[1])
|
|
|
|
if frequency == 'daily':
|
|
# Schedule daily at specific time
|
|
trigger = CronTrigger(hour=hour, minute=minute)
|
|
else: # weekly
|
|
# Map day name to cron day of week
|
|
days_map = {
|
|
'Monday': 'mon',
|
|
'Tuesday': 'tue',
|
|
'Wednesday': 'wed',
|
|
'Thursday': 'thu',
|
|
'Friday': 'fri',
|
|
'Saturday': 'sat',
|
|
'Sunday': 'sun'
|
|
}
|
|
cron_day = days_map.get(day_of_week, 'mon')
|
|
trigger = CronTrigger(day_of_week=cron_day, hour=hour, minute=minute)
|
|
|
|
scheduler.add_job(
|
|
execute_backup,
|
|
trigger,
|
|
id=job_id,
|
|
name=f"backup_schedule_{schedule_name}",
|
|
args=[schedule_id, schedule_name, backup_type],
|
|
replace_existing=True
|
|
)
|
|
|
|
logger.info(f"Registered schedule: {schedule_name} (ID: {schedule_id})")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to register schedule {schedule_name}: {e}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error loading schedules: {e}")
|
|
|
|
|
|
def init_scheduler(app):
|
|
"""Initialize the scheduler with Flask app"""
|
|
global scheduler
|
|
|
|
if scheduler is None:
|
|
scheduler = BackgroundScheduler(daemon=True)
|
|
|
|
# Load schedules from database
|
|
with app.app_context():
|
|
load_schedules()
|
|
|
|
# Start scheduler
|
|
scheduler.start()
|
|
logger.info("Backup scheduler initialized and started")
|
|
|
|
|
|
def shutdown_scheduler():
|
|
"""Shutdown the scheduler"""
|
|
global scheduler
|
|
|
|
if scheduler:
|
|
scheduler.shutdown()
|
|
scheduler = None
|
|
logger.info("Backup scheduler shut down")
|