Initial commit: Quality App v2 - FG Scan Module with Reports
This commit is contained in:
288
app/scheduler.py
Normal file
288
app/scheduler.py
Normal file
@@ -0,0 +1,288 @@
|
||||
"""
|
||||
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")
|
||||
Reference in New Issue
Block a user