Files
quality_app-v2/app/scheduler.py
2026-01-25 22:25:18 +02:00

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")