feat(backup)!: mandatory repo-name and databases-csv, --only-files, --only-sql

Two defaults could not be right. --repo-name fell back to the literal 'backup-docker-to-local' while its help promised the git repo folder name, which nothing ever derived. --databases-csv pointed inside the installed package directory, where credentials must not live; when it applied, load_databases_df read a missing file as empty and the run finished without a single dump and without an error. Both are required now, --repo-name in the restore CLI too. The file itself may still be absent - babadcb's tolerance is untouched, only the path must be named.

--everything is withdrawn. Its one effect was to ignore --images-no-stop-required, which is what leaving that list empty already does, and its branch was the default path minus the requires_stop check. No caller, no test, and help and README described it differently.

--dump-only-sql becomes --only-sql, and --only-files joins it as the opposite half: no dumps at all, every volume as files. They form a mutually exclusive group. A host that only copies files has no business holding database passwords, so --databases-csv is not required there and is never read.

The smallest valid argv turned out to be written four times across the test tree; it now lives once. Withdrawn flags are listed in one place and proven to exit 2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-17 13:54:39 +02:00
parent f791046c02
commit df1c65ccac
21 changed files with 330 additions and 135 deletions

View File

@@ -37,7 +37,7 @@ def main() -> int:
versions_dir = os.path.join(args.backups_dir, machine_id, args.repo_name)
version_dir = create_version_directory(versions_dir, backup_time)
databases_df = load_databases_df(args.databases_csv)
databases_df = None if args.only_files else load_databases_df(args.databases_csv)
print("💾 Start volume backups...", flush=True)
@@ -69,17 +69,19 @@ def main() -> int:
vol_dir = create_volume_directory(version_dir, volume_name)
found_db, dumped_any = backup_dumps_for_volume(
containers=containers,
vol_dir=vol_dir,
databases_df=databases_df,
database_containers=args.database_containers,
)
found_db = dumped_any = False
if not args.only_files:
found_db, dumped_any = backup_dumps_for_volume(
containers=containers,
vol_dir=vol_dir,
databases_df=databases_df,
database_containers=args.database_containers,
)
if args.dump_only_sql and found_db:
if args.only_sql and found_db:
if not dumped_any:
print(
f"WARNING: dump-only-sql requested but no DB dump was produced for DB volume '{volume_name}'. "
f"WARNING: only-sql requested but no DB dump was produced for DB volume '{volume_name}'. "
"Falling back to file backup.",
flush=True,
)
@@ -119,15 +121,6 @@ def main() -> int:
copy(authoritative=False)
continue
if args.everything:
stoppable = filter_stoppable(containers)
copy(authoritative=False)
change_containers_status(stoppable, "stop")
copy(authoritative=True)
if not args.shutdown:
change_containers_status(stoppable, "start")
continue
copy(authoritative=False)
if requires_stop(containers, args.images_no_stop_required):
stoppable = filter_stoppable(containers)

View File

@@ -1,13 +1,9 @@
from __future__ import annotations
import argparse
import os
def parse_args() -> argparse.Namespace:
dirname = os.path.dirname(__file__)
default_databases_csv = os.path.join(dirname, "databases.csv")
p = argparse.ArgumentParser(description="Backup Docker volumes.")
p.add_argument(
@@ -25,13 +21,12 @@ def parse_args() -> argparse.Namespace:
p.add_argument(
"--repo-name",
default="backup-docker-to-local",
help="Backup repo folder name under <backups-dir>/<machine-id>/ (default: git repo folder name)",
required=True,
help="Backup repo folder name under <backups-dir>/<machine-id>/",
)
p.add_argument(
"--databases-csv",
default=default_databases_csv,
help=f"Path to databases.csv (default: {default_databases_csv})",
help="Path to databases.csv; required unless --only-files is given",
)
p.add_argument(
"--backups-dir",
@@ -75,19 +70,15 @@ def parse_args() -> argparse.Namespace:
help="Exact volume names that are never backed up, whatever containers use them. For derived trees a restore cannot reproduce, above all a nested docker data root",
)
p.add_argument(
"--everything",
action="store_true",
help="Force file backup for all volumes and also execute database dumps (like old script)",
)
p.add_argument(
"--shutdown",
action="store_true",
help="Do not restart containers after backup",
)
p.add_argument(
"--dump-only-sql",
scope = p.add_mutually_exclusive_group()
scope.add_argument(
"--only-sql",
action="store_true",
help=(
"Create database dumps only for DB volumes. "
@@ -96,7 +87,19 @@ def parse_args() -> argparse.Namespace:
"If a DB dump cannot be produced, baudolo falls back to a file backup."
),
)
scope.add_argument(
"--only-files",
action="store_true",
help=(
"Take no database dumps at all and back up every volume as files. "
"For hosts that hold no database credentials. A database's files "
"are only consistent if its containers are stopped for the second "
"pass, so keep its image off --images-no-stop-required."
),
)
args = p.parse_args()
if not args.only_files and not args.databases_csv:
p.error("--databases-csv is required unless --only-files is given")
if bool(args.snapshot) != bool(args.snapshot_subject):
p.error("--snapshot and --snapshot-subject must be given together")
if args.snapshot and args.shutdown:

View File

@@ -22,8 +22,8 @@ def _add_common_backup_args(p: argparse.ArgumentParser) -> None:
)
p.add_argument(
"--repo-name",
default="backup-docker-to-local",
help="Backup repo folder name under <backups-dir>/<hash>/ (default: backup-docker-to-local)",
required=True,
help="Backup repo folder name under <backups-dir>/<hash>/",
)