diff --git a/app/sbin/api b/app/sbin/api index fc2c015..0aff93f 100755 --- a/app/sbin/api +++ b/app/sbin/api @@ -551,12 +551,30 @@ def backup_move(request: MoveRequest): class BackupCheckRequest(BaseModel): - uuid: str + uuid: Optional[str] = None @app.post("/backup/check", dependencies=[Depends(verify_api_key)]) def backup_check(request: BackupCheckRequest): - return {"message": run_command([f"{BIN_PATH}/checkBackup", request.uuid])} + if request.uuid: + return {"message": run_command([f"{BIN_PATH}/checkBackup", request.uuid])} + + # No uuid: check all active containers. checkBackup -a exits 1 when any + # backup is NOK, but its table is still the valid result, so don't raise. + result = subprocess.run( + [f"{BIN_PATH}/checkBackup", "-a"], capture_output=True, text=True + ) + if result.returncode != 0 and not result.stdout.strip(): + raise HTTPException( + status_code=500, + detail=f"checkBackup -a failed: {result.stderr.strip() or 'no output'}", + ) + return {"message": result.stdout} + + +@app.get("/containers/active", dependencies=[Depends(verify_api_key)]) +def containers_active(): + return {"message": run_command([f"{BIN_PATH}/activeContainers"])} @app.get("/backup/all", dependencies=[Depends(verify_api_key)]) diff --git a/app/sbin/checkBackup b/app/sbin/checkBackup index 7ed8571..f6dc041 100755 --- a/app/sbin/checkBackup +++ b/app/sbin/checkBackup @@ -1,44 +1,108 @@ #!/usr/bin/env python3 +"""Check the latest backup date. +Usage: + checkBackup print latest backup date for one container + checkBackup -a check all containers returned by activeContainers; + status is OK if the latest backup is at most 2 days + old, NOK if older or missing +""" + +import subprocess import sys +from datetime import date, datetime from pathlib import Path BACKUP_ROOT = Path("/BACKUP") +MAX_AGE_DAYS = 2 +ACTIVE_CONTAINERS = Path(__file__).resolve().parent / "activeContainers" -if len(sys.argv) < 2: - print("Usage: checkBackup ") - sys.exit(1) -uuid = sys.argv[1] -directory = BACKUP_ROOT / uuid +def check_backup(uuid): + """Return (ok, message) for the given UUID.""" + directory = BACKUP_ROOT / uuid -if not directory.is_dir(): - print(f"ERROR: directory not found for UUID {uuid}") - sys.exit(1) + if not directory.is_dir(): + return False, f"ERROR: directory not found for UUID {uuid}" -latest_date = None -latest_file = None + latest_date = None + latest_file = None -for zip_file in directory.rglob("*.zip"): - name = zip_file.name - try: - date_part = name.split("_")[0] + for zip_file in directory.rglob("*.zip"): + date_part = zip_file.name.split("_")[0] if len(date_part) != 8 or not date_part.isdigit(): continue - except Exception: - continue - if latest_date is None or date_part > latest_date: - latest_date = date_part - latest_file = zip_file + if latest_date is None or date_part > latest_date: + latest_date = date_part + latest_file = zip_file -if latest_date is None: - print("NO BACKUPS") - sys.exit(1) + if latest_date is None: + return False, "NO BACKUPS" -if latest_file.stat().st_size < 1000: - print("01-01-2000") - sys.exit(0) + if latest_file.stat().st_size < 1000: + return True, "01-01-2000" -# Format as yyyy-mm-dd -print(f"{latest_date[:4]}-{latest_date[4:6]}-{latest_date[6:8]}") + # Format as yyyy-mm-dd + return True, f"{latest_date[:4]}-{latest_date[4:6]}-{latest_date[6:8]}" + + +def is_recent(ok, message): + """True if the backup date in message is at most MAX_AGE_DAYS old.""" + if not ok: + return False + try: + backup_date = datetime.strptime(message, "%Y-%m-%d").date() + except ValueError: + # e.g. the "01-01-2000" placeholder for too-small backup files + return False + return (date.today() - backup_date).days <= MAX_AGE_DAYS + + +def active_uuids(): + """UUIDs printed by activeContainers (skips header and separator lines).""" + result = subprocess.run( + [str(ACTIVE_CONTAINERS)], capture_output=True, text=True + ) + if result.returncode != 0: + print( + f"ERROR: activeContainers failed: {result.stderr.strip()}", + file=sys.stderr, + ) + sys.exit(1) + + lines = result.stdout.splitlines() + uuids = [] + for line in lines[2:]: # line 0 = header, line 1 = "----" separator + fields = line.split() + if fields: + uuids.append(fields[0]) + return uuids + + +def main(): + if len(sys.argv) < 2: + print("Usage: checkBackup | -a") + sys.exit(1) + + if sys.argv[1] == "-a": + rows = [("uuid", "latest", "status")] + failed = False + for uuid in active_uuids(): + ok, message = check_backup(uuid) + recent = is_recent(ok, message) + rows.append((uuid, message, "OK" if recent else "NOK")) + failed = failed or not recent + + widths = [max(len(row[i]) for row in rows) for i in range(2)] + for uuid, message, status in rows: + print(f"{uuid:<{widths[0]}} {message:<{widths[1]}} {status}") + sys.exit(1 if failed else 0) + + ok, message = check_backup(sys.argv[1]) + print(message) + sys.exit(0 if ok else 1) + + +if __name__ == "__main__": + main() diff --git a/app/update_sbin b/app/update_sbin index d04fbae..8b6b872 100755 --- a/app/update_sbin +++ b/app/update_sbin @@ -15,6 +15,7 @@ rex doas chmod 600 /home/4server/.ssh/borg* # link all save scripts to /usr/sbin -rex doas ln -s /4server/sbin/activeContainers /usr/bin/ -rex doas ln -s /4server/sbin/startContainer /usr/bin/ -rex doas ln -s /4server/sbin/restartContainers /usr/bin/ +rex doas ln -sf /4server/sbin/activeContainers /usr/bin/ +rex doas ln -sf /4server/sbin/startContainer /usr/bin/ +rex doas ln -sf /4server/sbin/restartContainers /usr/bin/ +rex doas ln -sf /4server/sbin/checkBackup /usr/bin/