mydocker/side-agent/backup_scheduler.py

257 lines
9.5 KiB
Python

#!/usr/bin/env python3
import os
import subprocess
import schedule
import time
import logging
from datetime import datetime
from pathlib import Path
# Set up logging to match other side-agent scripts
logging.basicConfig(level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
class BackupScheduler:
def __init__(self):
self.asustor_host = "root@asustor-lan1"
self.log_dir = "/var/log"
# Ensure log directory exists
os.makedirs(self.log_dir, exist_ok=True)
logger.info("Backup scheduler initialized")
def run_rsync_command(self, source, destination, log_file, exclude_patterns=None, extra_args=None):
"""Run rsync command with proper error handling and logging."""
try:
# Base rsync command
cmd = ["/usr/bin/rsync", "-rltzuv"]
# Add exclude patterns
if exclude_patterns:
for pattern in exclude_patterns:
cmd.extend(["--exclude", pattern])
# Add extra arguments
if extra_args:
cmd.extend(extra_args)
# Add delete flags for most syncs
if "--no-delete" not in (extra_args or []):
cmd.extend(["--delete", "--delete-excluded"])
# Add source and destination
cmd.extend([source, f"{self.asustor_host}:{destination}"])
logger.info(f"Running rsync: {' '.join(cmd)}")
# Run the command and capture output
with open(log_file, 'w') as log:
result = subprocess.run(cmd, stdout=log, stderr=subprocess.STDOUT,
text=True, timeout=3600) # 1 hour timeout
if result.returncode == 0:
logger.info(f"Rsync completed successfully: {source} -> {destination}")
return True
else:
logger.error(f"Rsync failed with return code {result.returncode}")
return False
except subprocess.TimeoutExpired:
logger.error(f"Rsync timeout for {source} -> {destination}")
return False
except Exception as e:
logger.error(f"Error running rsync {source} -> {destination}: {e}")
return False
def rotate_mydocker_backup(self):
"""Create rotating backup of mydocker directory (3 days retention)."""
logger.info("Starting mydocker backup with rotation")
try:
# Create today's date-based directory name
today = datetime.now().strftime('%Y%m%d')
source = "/mydocker/"
base_dest = "/volume1/data/rsyncs/"
dest_dir = f"{base_dest}mydocker_{today}"
log_file = f"{self.log_dir}/rsync.mydocker.root2asustor.cron"
# Exclude patterns
exclude_patterns = [
'.git',
'*Library/Application Support/Plex Media Server*'
]
# Perform the rsync to today's backup directory
success = self.run_rsync_command(
source=source,
destination=f"{dest_dir}/",
log_file=log_file,
exclude_patterns=exclude_patterns
)
if success:
# Clean up old backups (keep only 3 most recent)
cleanup_cmd = [
"ssh", self.asustor_host,
f"find {base_dest} -maxdepth 1 -type d -name 'mydocker_*' | sort -r | tail -n +4 | xargs -r rm -rf"
]
subprocess.run(cleanup_cmd, check=False)
# Create symlink to latest backup
symlink_cmd = [
"ssh", self.asustor_host,
f"ln -sf {dest_dir} {base_dest}mydocker_latest"
]
subprocess.run(symlink_cmd, check=False)
logger.info("Mydocker backup rotation completed successfully")
else:
logger.error("Mydocker backup failed")
except Exception as e:
logger.error(f"Error in mydocker backup rotation: {e}")
def sync_myansible(self):
"""Sync myansible repository to asustor."""
logger.info("Starting myansible sync")
source = "/mydocker/../git.repos/myansible/"
destination = "/volume1/data/rsyncs/myansible/"
log_file = f"{self.log_dir}/rsync.myansible.root2asustor.cron"
exclude_patterns = ['.git']
self.run_rsync_command(
source=source,
destination=destination,
log_file=log_file,
exclude_patterns=exclude_patterns
)
def sync_myansible_to_gdrive(self):
"""Sync myansible to Google Drive location on asustor (weekly)."""
logger.info("Starting myansible to Google Drive sync")
source = "/mydocker/../git.repos/myansible/"
destination = "/volume1/data/google-drive-sync/backups_n_configs/rsyncs/"
log_file = f"{self.log_dir}/rsync.myansible.root2gcloud.cron"
exclude_patterns = ['.git']
self.run_rsync_command(
source=source,
destination=destination,
log_file=log_file,
exclude_patterns=exclude_patterns
)
def sync_hosts_file(self):
"""Sync /etc/hosts file to asustor."""
logger.info("Starting hosts file sync")
source = "/etc/hosts"
destination = "/volume1/data/rsyncs/hosts"
log_file = f"{self.log_dir}/rsync.hosts.root2asustor.cron"
self.run_rsync_command(
source=source,
destination=destination,
log_file=log_file,
extra_args=["--no-delete"] # Don't delete for single file
)
def sync_valuable_scripts(self):
"""Sync valuable scripts directory to asustor."""
logger.info("Starting valuable scripts sync")
source = "/mydocker/../valuable_scripts/"
destination = "/volume1/data/rsyncs/valuable_scripts/"
log_file = f"{self.log_dir}/rsync.valuable.scripts.root2asustor.cron"
exclude_patterns = ['.git']
self.run_rsync_command(
source=source,
destination=destination,
log_file=log_file,
exclude_patterns=exclude_patterns
)
def setup_schedules(self):
"""Set up all backup schedules matching the original Ansible cron jobs."""
# mydocker backup with rotation - daily at 7:33 AM
schedule.every().day.at("07:33").do(self.rotate_mydocker_backup)
# myansible sync - daily at 1:10 AM
schedule.every().day.at("01:10").do(self.sync_myansible)
# myansible to gdrive - weekly (same day as original)
schedule.every().sunday.at("01:15").do(self.sync_myansible_to_gdrive)
# hosts file sync - daily at 1:15 AM
schedule.every().day.at("01:15").do(self.sync_hosts_file)
# valuable scripts sync - daily at 1:15 AM
schedule.every().day.at("01:15").do(self.sync_valuable_scripts)
logger.info("All backup schedules configured")
def run_daemon(self):
"""Run the backup scheduler in daemon mode."""
logger.info("Starting backup scheduler daemon")
self.setup_schedules()
# Log the scheduled jobs
logger.info("Scheduled backup jobs:")
for job in schedule.jobs:
logger.info(f" - {job}")
while True:
try:
schedule.run_pending()
time.sleep(60) # Check every minute
except Exception as e:
logger.error(f"Error in backup scheduler daemon: {e}")
time.sleep(60) # Continue running even after errors
def main():
import argparse
parser = argparse.ArgumentParser(description='Backup scheduler for side-agent')
parser.add_argument('--daemon', action='store_true', help='Run in daemon mode')
parser.add_argument('--test-mydocker', action='store_true', help='Test mydocker backup')
parser.add_argument('--test-myansible', action='store_true', help='Test myansible sync')
parser.add_argument('--test-gdrive', action='store_true', help='Test gdrive sync')
parser.add_argument('--test-hosts', action='store_true', help='Test hosts sync')
parser.add_argument('--test-scripts', action='store_true', help='Test valuable scripts sync')
parser.add_argument('--test-all', action='store_true', help='Test all backup jobs')
args = parser.parse_args()
scheduler = BackupScheduler()
if args.daemon:
scheduler.run_daemon()
elif args.test_mydocker:
scheduler.rotate_mydocker_backup()
elif args.test_myansible:
scheduler.sync_myansible()
elif args.test_gdrive:
scheduler.sync_myansible_to_gdrive()
elif args.test_hosts:
scheduler.sync_hosts_file()
elif args.test_scripts:
scheduler.sync_valuable_scripts()
elif args.test_all:
logger.info("Testing all backup jobs...")
scheduler.rotate_mydocker_backup()
scheduler.sync_myansible()
scheduler.sync_hosts_file()
scheduler.sync_valuable_scripts()
scheduler.sync_myansible_to_gdrive()
else:
parser.print_help()
if __name__ == "__main__":
main()