mirror of
https://github.com/kevinveenbirkenbach/docker-volume-backup.git
synced 2025-07-04 15:53:09 +02:00
Added restore_backup.py restore_postgres_databases.py
This commit is contained in:
parent
556cb17433
commit
5762754ed7
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()
|
Loading…
x
Reference in New Issue
Block a user