feat: implement PostgreSQL to MinIO backup solution
Introduced a new Python script to back up PostgreSQL databases, encrypt the dumps using GPG, and then upload them to MinIO. This solution includes: - `.gitignore` to safeguard against accidentally committing sensitive files. - A clear `LICENSE` and `README.md` for legal clarity and usage documentation. - Sample configuration in `config.dist.yaml` to ease setup. - Defined project dependencies and metadata in `pyproject.toml` for streamlined package management. - Core implementation in `src/postgres_minio_backup` to encapsulate backup logic. This commit sets the foundation for a robust, secure database backup mechanism, facilitating easier disaster recovery and data protection strategies.
This commit is contained in:
commit
828fb667f2
7 changed files with 239 additions and 0 deletions
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
.venv
|
||||||
|
config.yaml
|
||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
.ruff_cache/
|
||||||
|
dist/
|
||||||
|
venv/
|
19
LICENSE
Normal file
19
LICENSE
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
Copyright (c) 2024 Kumi Mitterer <postgres-minio-backup@kumi.email>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
27
README.md
Normal file
27
README.md
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
# PostgreSQL to MinIO Backup
|
||||||
|
|
||||||
|
This is a simple script to backup a PostgreSQL database, encrypt the dump using GPG, and upload it to MinIO.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Set up a Python virtual environment and install the requirements:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install -U .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
First, set up a `config.yaml` based on the template provided in [config.dist.yaml](config.dist.yaml).
|
||||||
|
|
||||||
|
Then, run the script:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
postgres-minio-backup
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
18
config.dist.yaml
Normal file
18
config.dist.yaml
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
postgres_hosts:
|
||||||
|
- host: 'postgres_host_1'
|
||||||
|
port: '5432'
|
||||||
|
user: 'postgres_user_1'
|
||||||
|
password: 'postgres_password_1'
|
||||||
|
- host: 'postgres_host_2'
|
||||||
|
port: '5432'
|
||||||
|
user: 'postgres_user_2'
|
||||||
|
password: 'postgres_password_2'
|
||||||
|
|
||||||
|
minio:
|
||||||
|
endpoint: 'your_minio_endpoint'
|
||||||
|
access_key: 'your_minio_access_key'
|
||||||
|
secret_key: 'your_minio_secret_key'
|
||||||
|
bucket_name: 'your_minio_bucket_name'
|
||||||
|
|
||||||
|
gpg:
|
||||||
|
recipient: 'recipient@example.com'
|
31
pyproject.toml
Normal file
31
pyproject.toml
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "postgres-minio-backup"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = [
|
||||||
|
{ name="Kumi Mitterer", email="postgres-minio-backup@kumi.email" },
|
||||||
|
]
|
||||||
|
description = "Simple Python script to backup a PostgreSQL database to MinIO"
|
||||||
|
readme = "README.md"
|
||||||
|
license = { file="LICENSE" }
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
classifiers = [
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"License :: OSI Approved :: MIT License",
|
||||||
|
"Operating System :: POSIX",
|
||||||
|
]
|
||||||
|
dependencies = [
|
||||||
|
"psycopg2-binary",
|
||||||
|
"boto3",
|
||||||
|
"pyyaml",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
postgres-minio-backup = "postgres_minio_backup:main"
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
"Homepage" = "https://git.private.coffee/kumi/postgres-minio-backup"
|
||||||
|
"Bug Tracker" = "https://git.private.coffee/kumi/postgres-minio-backup/issues"
|
1
src/postgres_minio_backup/__init__.py
Normal file
1
src/postgres_minio_backup/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
from .__main__ import main # noqa: F401
|
136
src/postgres_minio_backup/__main__.py
Normal file
136
src/postgres_minio_backup/__main__.py
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
import psycopg2
|
||||||
|
import subprocess
|
||||||
|
import boto3
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import yaml
|
||||||
|
import argparse
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
from botocore.client import Config
|
||||||
|
|
||||||
|
log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||||
|
logging.basicConfig(format=log_format)
|
||||||
|
|
||||||
|
|
||||||
|
def load_config(config_path):
|
||||||
|
"""Load configuration from a YAML file."""
|
||||||
|
with open(config_path, "r") as config_file:
|
||||||
|
return yaml.safe_load(config_file)
|
||||||
|
|
||||||
|
|
||||||
|
def run_backup(config_path):
|
||||||
|
"""Backup PostgreSQL databases and upload to MinIO."""
|
||||||
|
try:
|
||||||
|
config = load_config(config_path)
|
||||||
|
except FileNotFoundError:
|
||||||
|
logging.fatal(f"Configuration file not found: {config_path}")
|
||||||
|
return
|
||||||
|
|
||||||
|
minio_config = config["minio"]
|
||||||
|
gpg_recipient = config["gpg"]["recipient"]
|
||||||
|
|
||||||
|
# Initialize MinIO client
|
||||||
|
s3_client = boto3.client(
|
||||||
|
"s3",
|
||||||
|
endpoint_url=minio_config["endpoint"],
|
||||||
|
aws_access_key_id=minio_config["access_key"],
|
||||||
|
aws_secret_access_key=minio_config["secret_key"],
|
||||||
|
config=Config(signature_version="s3v4"),
|
||||||
|
)
|
||||||
|
|
||||||
|
for pg_host in config["postgres_hosts"]:
|
||||||
|
db_host = pg_host["host"]
|
||||||
|
db_port = pg_host["port"]
|
||||||
|
db_user = pg_host["user"]
|
||||||
|
db_password = pg_host["password"]
|
||||||
|
|
||||||
|
# Connect to the PostgreSQL server
|
||||||
|
conn = psycopg2.connect(
|
||||||
|
dbname="postgres",
|
||||||
|
user=db_user,
|
||||||
|
password=db_password,
|
||||||
|
host=db_host,
|
||||||
|
port=db_port,
|
||||||
|
)
|
||||||
|
conn.autocommit = True
|
||||||
|
|
||||||
|
# Get a list of all databases
|
||||||
|
with conn.cursor() as cursor:
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT datname FROM pg_database WHERE datistemplate = false;"
|
||||||
|
)
|
||||||
|
databases = cursor.fetchall()
|
||||||
|
|
||||||
|
for db in databases:
|
||||||
|
db_name = db[0]
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
|
||||||
|
dump_file = os.path.join(tmpdir, f"{db_name}-{timestamp}.sql")
|
||||||
|
gzip_file = f"{dump_file}.gz"
|
||||||
|
gpg_file = f"{gzip_file}.gpg"
|
||||||
|
|
||||||
|
# Dump the database
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
"pg_dump",
|
||||||
|
"-h",
|
||||||
|
db_host,
|
||||||
|
"-p",
|
||||||
|
db_port,
|
||||||
|
"-U",
|
||||||
|
db_user,
|
||||||
|
"-F",
|
||||||
|
"c",
|
||||||
|
"-f",
|
||||||
|
dump_file,
|
||||||
|
db_name,
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Gzip the dump file
|
||||||
|
subprocess.run(["gzip", dump_file], check=True)
|
||||||
|
|
||||||
|
# GPG encrypt the gzip file
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
"gpg",
|
||||||
|
"--output",
|
||||||
|
gpg_file,
|
||||||
|
"--encrypt",
|
||||||
|
"--recipient",
|
||||||
|
gpg_recipient,
|
||||||
|
gzip_file,
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Upload to MinIO
|
||||||
|
with open(gpg_file, "rb") as f:
|
||||||
|
s3_client.upload_fileobj(
|
||||||
|
f, minio_config["bucket_name"], os.path.basename(gpg_file)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Close the connection
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Backup PostgreSQL databases and upload to MinIO."
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--config",
|
||||||
|
type=str,
|
||||||
|
default="config.yaml",
|
||||||
|
help="Path to the configuration file",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
run_backup(args.config)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
Loading…
Reference in a new issue