257 lines
9.5 KiB
Python
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()
|