diff --git a/README.md b/README.md index 0aba12cae3013ed9244858f0d9be202249b94862..96b851ae573b41b71fe018e48156db137da82408 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,15 @@ rwm restic snapshots rwm restic mount /mnt/restore ``` +### Restore PostgreSQL backup + +``` +# Using script for the whole archive +scripts/restore_postgresql.sh postgresql.tar.gz + +# For individual dumps +createdb database_name && gunzip -c database_name.sql.gz | psql -d database_name +``` ## Notes diff --git a/examples/rwm-backups.conf b/examples/rwm-backups.conf index 07464d2a9393a34eb4dee54cd27827a84bd65b47..44dce21e95f1f5cdae1050f8e30119d53746d5f3 100644 --- a/examples/rwm-backups.conf +++ b/examples/rwm-backups.conf @@ -31,6 +31,16 @@ backups: postrun: - "/opt/rwm/scripts/backup_mysql.py cleanup" + postgresql: + filesdirs: + - /var/lib/rwm/postgresql.tar.gz + tags: + - "postgresql" + prerun: + - "/opt/rwm/scripts/backup_postgresql.py create" + postrun: + - "/opt/rwm/scripts/backup_postgresql.py cleanup" + retention: keep-daily: "60" keep-within: "60d" diff --git a/scripts/backup_postgresql.py b/scripts/backup_postgresql.py new file mode 100755 index 0000000000000000000000000000000000000000..c6a5a4b0abb3f8ba38660bb5d39ea140b6ae3aa3 --- /dev/null +++ b/scripts/backup_postgresql.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +"""rwm postgresql backup helper""" + +import gzip +import os +import shutil +import subprocess +import sys +from argparse import ArgumentParser + + +BASE = "/var/lib/rwm" +BACKUPDIR = f"{BASE}/postgresql" +ARCHIVE = f"{BASE}/postgresql.tar.gz" +USERNAME = os.environ.get("PGUSER", "postgres") + + +def list_databases(): + """list postgresql databases""" + + cmd = ["psql", "-U", USERNAME, "-h", "127.0.0.1", "-q", "-A", "-t", "-c", "SELECT datname FROM pg_database WHERE datistemplate = false;"] + + proc = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, text=True) + + return proc.stdout.splitlines() + + +def backup_database(database): + """backup single database and compress it""" + + cmd = ["pg_dump", "-U", USERNAME, "-h", "127.0.0.1", database] + + try: + with gzip.open(f"{BACKUPDIR}/{database}.sql.gz", "wb") as fd: + with subprocess.Popen(cmd, stdout=subprocess.PIPE, universal_newlines=True) as proc: + for stdout_line in iter(proc.stdout.readline, ''): + fd.write(stdout_line.encode('utf-8')) + + # Dumps roles + if database == "postgres": + roles_cmd = ["pg_dumpall", "-U", USERNAME, "-h", "127.0.0.1", "--roles-only"] + with subprocess.Popen(roles_cmd, stdout=subprocess.PIPE, universal_newlines=True) as roles_proc: + for stdout_line in iter(roles_proc.stdout.readline, ''): + fd.write(stdout_line.encode('utf-8')) + + except subprocess.CalledProcessError: + print(f"ERROR: cannot dump {database}", file=sys.stderr) + return 1 + + return 0 + + +def create(): + """dump database to archive""" + + databases = 0 + errors = 0 + + shutil.rmtree(BACKUPDIR, ignore_errors=True) + os.makedirs(BACKUPDIR, exist_ok=True) + + for db in list_databases(): + databases += 1 + errors += backup_database(db) + + subprocess.run(["tar", "czf", ARCHIVE, BACKUPDIR], check=True) + shutil.rmtree(BACKUPDIR) + print("archive created:") + subprocess.run(["ls", "-l", ARCHIVE], check=True) + + if databases == 0: + print("ERROR: no databases dumped", file=sys.stderr) + errors += 1 + + print(f"RESULT: errors={errors} databases={databases}") + return errors + + +def cleanup(): + """cleanup backup process""" + + return os.unlink(ARCHIVE) + + +# pylint: disable=duplicate-code +def main(): + """main""" + + parser = ArgumentParser() + parser.add_argument("command", choices=["create", "cleanup"]) + args = parser.parse_args() + os.umask(0o077) + + if args.command == "create": + return create() + if args.command == "cleanup": + return cleanup() + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/restore_postgresql.sh b/scripts/restore_postgresql.sh new file mode 100755 index 0000000000000000000000000000000000000000..ced0809bd22f91e4f51f41c9b664b956a14ec404 --- /dev/null +++ b/scripts/restore_postgresql.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +if [ "$#" -ne 1 ]; then + echo "Usage: $0 <tar_file>" + exit 1 +fi + +tar_file="$1" + +if [ ! -f "$tar_file" ]; then + echo "Error: '$tar_file' does not exist." + exit 1 +fi + +temp_dir=$(mktemp -d) + +tar -xzf "$tar_file" -C "$temp_dir" + +sql_files=$(find "$temp_dir" -type f -name '*.sql.gz') + +for dump_file in $sql_files; do + db_name=$(basename "$dump_file" .sql.gz) + + createdb "$db_name" + + gunzip -c "$dump_file" | psql -q -d "$db_name" +done + +rm -rf "$temp_dir" + +echo "Databases restored."