mirror of
				https://github.com/kevinveenbirkenbach/docker-volume-backup.git
				synced 2025-11-04 10:38:00 +00:00 
			
		
		
		
	Added restore_backup.py restore_postgres_databases.py
This commit is contained in:
		
							
								
								
									
										2
									
								
								Todo.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								Todo.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					# Todo
 | 
				
			||||||
 | 
					- Verify that restore backup is correct implemented
 | 
				
			||||||
							
								
								
									
										170
									
								
								restore_backup.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								restore_backup.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,170 @@
 | 
				
			|||||||
 | 
					#!/usr/bin/env python3
 | 
				
			||||||
 | 
					# @todo Not tested yet. Needs to be tested
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					restore_backup.py
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					A script to recover Docker volumes and database dumps from local backups.
 | 
				
			||||||
 | 
					Supports an --empty flag to clear the database objects before import (drops all tables/functions etc.).
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					import argparse
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					import sys
 | 
				
			||||||
 | 
					import subprocess
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def run_command(cmd, capture_output=False, input=None, **kwargs):
 | 
				
			||||||
 | 
					    """Run a subprocess command and handle errors."""
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        result = subprocess.run(cmd, check=True, capture_output=capture_output, input=input, **kwargs)
 | 
				
			||||||
 | 
					        return result
 | 
				
			||||||
 | 
					    except subprocess.CalledProcessError as e:
 | 
				
			||||||
 | 
					        print(f"ERROR: Command '{' '.join(cmd)}' failed with exit code {e.returncode}")
 | 
				
			||||||
 | 
					        if e.stdout:
 | 
				
			||||||
 | 
					            print(e.stdout.decode())
 | 
				
			||||||
 | 
					        if e.stderr:
 | 
				
			||||||
 | 
					            print(e.stderr.decode())
 | 
				
			||||||
 | 
					        sys.exit(1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def recover_postgres(container, password, db_name, user, backup_sql, empty=False):
 | 
				
			||||||
 | 
					    print("Recovering PostgreSQL dump...")
 | 
				
			||||||
 | 
					    os.environ['PGPASSWORD'] = password
 | 
				
			||||||
 | 
					    if empty:
 | 
				
			||||||
 | 
					        print("Dropping existing PostgreSQL objects...")
 | 
				
			||||||
 | 
					        # Drop all tables, views, sequences, functions in public schema
 | 
				
			||||||
 | 
					        drop_sql = """
 | 
				
			||||||
 | 
					DO $$ DECLARE r RECORD;
 | 
				
			||||||
 | 
					BEGIN
 | 
				
			||||||
 | 
					  FOR r IN (
 | 
				
			||||||
 | 
					    SELECT table_name AS name, 'TABLE' AS type FROM information_schema.tables WHERE table_schema='public'
 | 
				
			||||||
 | 
					    UNION ALL
 | 
				
			||||||
 | 
					    SELECT routine_name AS name, 'FUNCTION' AS type FROM information_schema.routines WHERE specific_schema='public'
 | 
				
			||||||
 | 
					    UNION ALL
 | 
				
			||||||
 | 
					    SELECT sequence_name AS name, 'SEQUENCE' AS type FROM information_schema.sequences WHERE sequence_schema='public'
 | 
				
			||||||
 | 
					  ) LOOP
 | 
				
			||||||
 | 
					    -- Use %s for type to avoid quoting the SQL keyword
 | 
				
			||||||
 | 
					    EXECUTE format('DROP %s public.%I CASCADE', r.type, r.name);
 | 
				
			||||||
 | 
					  END LOOP;
 | 
				
			||||||
 | 
					END
 | 
				
			||||||
 | 
					$$;
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					        run_command([
 | 
				
			||||||
 | 
					            'docker', 'exec', '-i', container,
 | 
				
			||||||
 | 
					            'psql', '-v', 'ON_ERROR_STOP=1', '-U', user, '-d', db_name
 | 
				
			||||||
 | 
					        ], input=drop_sql.encode())
 | 
				
			||||||
 | 
					        print("Existing objects dropped.")
 | 
				
			||||||
 | 
					    print("Importing the dump...")
 | 
				
			||||||
 | 
					    with open(backup_sql, 'rb') as f:
 | 
				
			||||||
 | 
					        run_command([
 | 
				
			||||||
 | 
					            'docker', 'exec', '-i', container,
 | 
				
			||||||
 | 
					            'psql', '-v', 'ON_ERROR_STOP=1', '-U', user, '-d', db_name
 | 
				
			||||||
 | 
					        ], stdin=f)
 | 
				
			||||||
 | 
					    print("PostgreSQL recovery complete.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def recover_mariadb(container, password, db_name, user, backup_sql, empty=False):
 | 
				
			||||||
 | 
					    print("Recovering MariaDB dump...")
 | 
				
			||||||
 | 
					    if empty:
 | 
				
			||||||
 | 
					        print("Dropping existing MariaDB tables...")
 | 
				
			||||||
 | 
					        # Disable foreign key checks
 | 
				
			||||||
 | 
					        run_command([
 | 
				
			||||||
 | 
					            'docker', 'exec', container,
 | 
				
			||||||
 | 
					            'mysql', '-u', user, f"--password={password}", '-e', 'SET FOREIGN_KEY_CHECKS=0;'
 | 
				
			||||||
 | 
					        ])
 | 
				
			||||||
 | 
					        # Get all table names
 | 
				
			||||||
 | 
					        result = run_command([
 | 
				
			||||||
 | 
					            'docker', 'exec', container,
 | 
				
			||||||
 | 
					            'mysql', '-u', user, f"--password={password}", '-N', '-e',
 | 
				
			||||||
 | 
					            f"SELECT table_name FROM information_schema.tables WHERE table_schema = '{db_name}';"
 | 
				
			||||||
 | 
					        ], capture_output=True)
 | 
				
			||||||
 | 
					        tables = result.stdout.decode().split()
 | 
				
			||||||
 | 
					        for tbl in tables:
 | 
				
			||||||
 | 
					            run_command([
 | 
				
			||||||
 | 
					                'docker', 'exec', container,
 | 
				
			||||||
 | 
					                'mysql', '-u', user, f"--password={password}", '-e',
 | 
				
			||||||
 | 
					                f"DROP TABLE IF EXISTS `{db_name}`.`{tbl}`;"
 | 
				
			||||||
 | 
					            ])
 | 
				
			||||||
 | 
					        # Enable foreign key checks
 | 
				
			||||||
 | 
					        run_command([
 | 
				
			||||||
 | 
					            'docker', 'exec', container,
 | 
				
			||||||
 | 
					            'mysql', '-u', user, f"--password={password}", '-e', 'SET FOREIGN_KEY_CHECKS=1;'
 | 
				
			||||||
 | 
					        ])
 | 
				
			||||||
 | 
					        print("Existing tables dropped.")
 | 
				
			||||||
 | 
					    print("Importing the dump...")
 | 
				
			||||||
 | 
					    with open(backup_sql, 'rb') as f:
 | 
				
			||||||
 | 
					        run_command([
 | 
				
			||||||
 | 
					            'docker', 'exec', '-i', container,
 | 
				
			||||||
 | 
					            'mariadb', '-u', user, f"--password={password}", db_name
 | 
				
			||||||
 | 
					        ], stdin=f)
 | 
				
			||||||
 | 
					    print("MariaDB recovery complete.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def recover_files(volume_name, backup_files):
 | 
				
			||||||
 | 
					    print(f"Inspecting volume {volume_name}...")
 | 
				
			||||||
 | 
					    inspect = subprocess.run(['docker', 'volume', 'inspect', volume_name], stdout=subprocess.DEVNULL)
 | 
				
			||||||
 | 
					    if inspect.returncode != 0:
 | 
				
			||||||
 | 
					        print(f"Volume {volume_name} does not exist. Creating...")
 | 
				
			||||||
 | 
					        run_command(['docker', 'volume', 'create', volume_name])
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        print(f"Volume {volume_name} already exists.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not os.path.isdir(backup_files):
 | 
				
			||||||
 | 
					        print(f"ERROR: Backup files folder '{backup_files}' does not exist.")
 | 
				
			||||||
 | 
					        sys.exit(1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    print("Recovering files...")
 | 
				
			||||||
 | 
					    run_command([
 | 
				
			||||||
 | 
					        'docker', 'run', '--rm',
 | 
				
			||||||
 | 
					        '-v', f"{volume_name}:/recover/",
 | 
				
			||||||
 | 
					        '-v', f"{backup_files}:/backup/",
 | 
				
			||||||
 | 
					        'kevinveenbirkenbach/alpine-rsync',
 | 
				
			||||||
 | 
					        'sh', '-c', 'rsync -avv --delete /backup/ /recover/'
 | 
				
			||||||
 | 
					    ])
 | 
				
			||||||
 | 
					    print("File recovery complete.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def main():
 | 
				
			||||||
 | 
					    parser = argparse.ArgumentParser(
 | 
				
			||||||
 | 
					        description='Recover Docker volumes and database dumps from local backups.'
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    parser.add_argument('volume_name', help='Name of the Docker volume')
 | 
				
			||||||
 | 
					    parser.add_argument('backup_hash', help='Hashed Machine ID')
 | 
				
			||||||
 | 
					    parser.add_argument('version', help='Version to recover')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    parser.add_argument('--db-type', choices=['postgres', 'mariadb'], help='Type of database backup')
 | 
				
			||||||
 | 
					    parser.add_argument('--db-container', help='Docker container name for the database')
 | 
				
			||||||
 | 
					    parser.add_argument('--db-password', help='Password for the database user')
 | 
				
			||||||
 | 
					    parser.add_argument('--db-name', help='Name of the database')
 | 
				
			||||||
 | 
					    parser.add_argument('--empty', action='store_true', help='Drop existing database objects before importing')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    args = parser.parse_args()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    volume = args.volume_name
 | 
				
			||||||
 | 
					    backup_hash = args.backup_hash
 | 
				
			||||||
 | 
					    version = args.version
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    backup_folder = os.path.join('Backups', backup_hash, 'backup-docker-to-local', version, volume)
 | 
				
			||||||
 | 
					    backup_files = os.path.join(os.sep, backup_folder, 'files')
 | 
				
			||||||
 | 
					    backup_sql = None
 | 
				
			||||||
 | 
					    if args.db_name:
 | 
				
			||||||
 | 
					        backup_sql = os.path.join(os.sep, backup_folder, 'sql', f"{args.db_name}.backup.sql")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Database recovery
 | 
				
			||||||
 | 
					    if args.db_type:
 | 
				
			||||||
 | 
					        if not (args.db_container and args.db_password and args.db_name):
 | 
				
			||||||
 | 
					            print("ERROR: A database backup exists, aber ein Parameter fehlt.")
 | 
				
			||||||
 | 
					            sys.exit(1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        user = args.db_name
 | 
				
			||||||
 | 
					        if args.db_type == 'postgres':
 | 
				
			||||||
 | 
					            recover_postgres(args.db_container, args.db_password, args.db_name, user, backup_sql, empty=args.empty)
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            recover_mariadb(args.db_container, args.db_password, args.db_name, user, backup_sql, empty=args.empty)
 | 
				
			||||||
 | 
					        sys.exit(0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # File recovery
 | 
				
			||||||
 | 
					    recover_files(volume, backup_files)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if __name__ == '__main__':
 | 
				
			||||||
 | 
					    main()
 | 
				
			||||||
							
								
								
									
										83
									
								
								restore_postgres_databases.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								restore_postgres_databases.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,83 @@
 | 
				
			|||||||
 | 
					#!/usr/bin/env python3
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Restore multiple PostgreSQL databases from .backup.sql files via a Docker container.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Usage:
 | 
				
			||||||
 | 
					  ./restore_databases.py /path/to/backup_dir [--container central-postgres]
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					import argparse
 | 
				
			||||||
 | 
					import subprocess
 | 
				
			||||||
 | 
					import sys
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					import glob
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def run_command(cmd, input_data=None):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Run a subprocess command and exit on failure.
 | 
				
			||||||
 | 
					    :param cmd: list of command parts
 | 
				
			||||||
 | 
					    :param input_data: bytes to send to process stdin
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        subprocess.run(cmd, input=input_data, check=True)
 | 
				
			||||||
 | 
					    except subprocess.CalledProcessError as e:
 | 
				
			||||||
 | 
					        print(f"Error running command: {' '.join(cmd)}", file=sys.stderr)
 | 
				
			||||||
 | 
					        sys.exit(e.returncode)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def main():
 | 
				
			||||||
 | 
					    parser = argparse.ArgumentParser(
 | 
				
			||||||
 | 
					        description="Restore Postgres databases from backup SQL files via Docker container."
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    parser.add_argument(
 | 
				
			||||||
 | 
					        "backup_dir",
 | 
				
			||||||
 | 
					        help="Path to directory containing .backup.sql files"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    parser.add_argument(
 | 
				
			||||||
 | 
					        "container",
 | 
				
			||||||
 | 
					        help="Name of the Postgres Docker container"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    args = parser.parse_args()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    backup_dir = args.backup_dir
 | 
				
			||||||
 | 
					    container = args.container
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pattern = os.path.join(backup_dir, "*.backup.sql")
 | 
				
			||||||
 | 
					    sql_files = sorted(glob.glob(pattern))
 | 
				
			||||||
 | 
					    if not sql_files:
 | 
				
			||||||
 | 
					        print(f"No .backup.sql files found in {backup_dir}", file=sys.stderr)
 | 
				
			||||||
 | 
					        sys.exit(1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for sqlfile in sql_files:
 | 
				
			||||||
 | 
					        dbname = os.path.splitext(os.path.basename(sqlfile))[0]
 | 
				
			||||||
 | 
					        print(f"=== Processing {sqlfile} → database: {dbname} ===")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Drop the database if it already exists
 | 
				
			||||||
 | 
					        run_command([
 | 
				
			||||||
 | 
					            "docker", "exec", "-i", container,
 | 
				
			||||||
 | 
					            "psql", "-U", "postgres", "-c",
 | 
				
			||||||
 | 
					            f"DROP DATABASE IF EXISTS \"{dbname}\";"
 | 
				
			||||||
 | 
					        ])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Create a fresh database
 | 
				
			||||||
 | 
					        run_command([
 | 
				
			||||||
 | 
					            "docker", "exec", "-i", container,
 | 
				
			||||||
 | 
					            "psql", "-U", "postgres", "-c",
 | 
				
			||||||
 | 
					            f"CREATE DATABASE \"{dbname}\";"
 | 
				
			||||||
 | 
					        ])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Restore the dump into the newly created database
 | 
				
			||||||
 | 
					        print(f"Restoring dump into {dbname}…")
 | 
				
			||||||
 | 
					        with open(sqlfile, "rb") as f:
 | 
				
			||||||
 | 
					            sql_data = f.read()
 | 
				
			||||||
 | 
					        run_command([
 | 
				
			||||||
 | 
					            "docker", "exec", "-i", container,
 | 
				
			||||||
 | 
					            "psql", "-U", "postgres", "-d", dbname
 | 
				
			||||||
 | 
					        ], input_data=sql_data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        print(f"✔ {dbname} restored.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    print("All databases have been restored.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if __name__ == "__main__":
 | 
				
			||||||
 | 
					    main()
 | 
				
			||||||
		Reference in New Issue
	
	Block a user