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