feat: add json output for backup plans

This commit is contained in:
Eddie Nielsen 2026-03-24 14:46:37 +00:00
parent 2a8d7fc8b7
commit 1d2dc54964

View file

@ -1,4 +1,5 @@
import argparse import argparse
import json
import sys import sys
from pathlib import Path from pathlib import Path
@ -49,7 +50,7 @@ def entry_to_dict(entry):
"exists": data.get("exists"), "exists": data.get("exists"),
"reason": data.get("reason"), "reason": data.get("reason"),
"compose_file": compose_file, "compose_file": compose_file,
"source": data.get("source"), "source": str(data.get("source")) if data.get("source") is not None else None,
} }
@ -84,12 +85,23 @@ def normalize_entries(entries):
def build_plan(scan_root, entries, compose_count): def build_plan(scan_root, entries, compose_count):
normalized = normalize_entries(entries) normalized = normalize_entries(entries)
return { return {
"root": str(scan_root), "root": str(scan_root),
"compose_files_found": compose_count, "compose_files_found": compose_count,
"include": [e for e in normalized if e["bucket"] == "include"], "include": [e for e in normalized if e["bucket"] == "include"],
"review": [e for e in normalized if e["bucket"] == "review"], "review": [e for e in normalized if e["bucket"] == "review"],
"skip": [e for e in normalized if e["bucket"] == "skip"], "skip": [e for e in normalized if e["bucket"] == "skip"],
"summary": {
"include_count": sum(1 for e in normalized if e["bucket"] == "include"),
"review_count": sum(1 for e in normalized if e["bucket"] == "review"),
"skip_count": sum(1 for e in normalized if e["bucket"] == "skip"),
"missing_critical_count": sum(
1
for e in normalized
if e["bucket"] == "include" and not e.get("exists", True)
),
},
} }
@ -141,6 +153,25 @@ def print_warnings(plan):
print(f" - {item['path']} (service={item['service']})") print(f" - {item['path']} (service={item['service']})")
def build_json_output(plan, borg_command=None):
output = {
"tool": "DockerVault",
"version": __version__,
"plan": plan,
"status": {
"ok": True,
"has_missing_critical": any(
not item.get("exists", True) for item in plan["include"]
),
},
}
if borg_command is not None:
output["borg_command"] = borg_command
return output
def build_parser(): def build_parser():
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="DockerVault - Intelligent Docker backup discovery" description="DockerVault - Intelligent Docker backup discovery"
@ -169,6 +200,12 @@ def build_parser():
help="Generate borg command", help="Generate borg command",
) )
parser.add_argument(
"--json",
action="store_true",
help="Output machine-readable JSON",
)
parser.add_argument( parser.add_argument(
"--dry-run", "--dry-run",
action="store_true", action="store_true",
@ -237,10 +274,7 @@ def main():
plan = build_plan(scan_root, entries, len(compose_files)) plan = build_plan(scan_root, entries, len(compose_files))
if not args.quiet: borg_shell_command = None
print_plan(plan)
print_warnings(plan)
if args.repo: if args.repo:
try: try:
from .borg import build_borg_create_command, command_to_shell from .borg import build_borg_create_command, command_to_shell
@ -249,16 +283,33 @@ def main():
sys.exit(2) sys.exit(2)
try: try:
include_paths = [item["path"] for item in plan["include"] if item["path"] != "?"] include_paths = [
item["path"] for item in plan["include"] if item["path"] != "?"
]
borg_command = build_borg_create_command(args.repo, include_paths) borg_command = build_borg_create_command(args.repo, include_paths)
borg_shell_command = command_to_shell(borg_command)
print("\nSuggested borg create command")
print("============================")
print(command_to_shell(borg_command))
except Exception as e: except Exception as e:
print(f"ERROR: Failed to build borg command: {e}") print(f"ERROR: Failed to build borg command: {e}")
sys.exit(2) sys.exit(2)
if args.json:
print(
json.dumps(
build_json_output(plan, borg_shell_command),
indent=2,
sort_keys=False,
)
)
else:
if not args.quiet:
print_plan(plan)
print_warnings(plan)
if borg_shell_command:
print("\nSuggested borg create command")
print("============================")
print(borg_shell_command)
if args.automation: if args.automation:
has_missing = any(not item.get("exists", True) for item in plan["include"]) has_missing = any(not item.get("exists", True) for item in plan["include"])
if has_missing: if has_missing: