diff --git a/app/host_vars/hosts b/app/host_vars/hosts index c58c6d8..66e0db1 100644 --- a/app/host_vars/hosts +++ b/app/host_vars/hosts @@ -1 +1 @@ -sydney +sydney2 diff --git a/app/onboarding b/app/onboarding index 1be4fd2..61c564e 100755 --- a/app/onboarding +++ b/app/onboarding @@ -71,4 +71,3 @@ rex doas mkdir -p /etc/doas.d rex doas rc-service sshd restart rex doas rc-service nebula restart -rex doas reboot diff --git a/app/sbin/api b/app/sbin/api index 011a894..0b5faf8 100755 --- a/app/sbin/api +++ b/app/sbin/api @@ -396,6 +396,15 @@ def server_audit(): return {"raw_output": output} +@app.get("/server/stats", dependencies=[Depends(verify_api_key)]) +def server_stats(): + output = run_command([f"{BIN_PATH}/stats"]) + try: + return json.loads(output) + except json.JSONDecodeError: + return {"raw_output": output} + + @app.post("/system/pull", dependencies=[Depends(verify_api_key)]) def pull_all_images(): return {"message": run_command([f"{BIN_PATH}/pullAllContainers"])} @@ -550,6 +559,11 @@ def backup_check(request: BackupCheckRequest): return {"message": run_command([f"{BIN_PATH}/checkBackup", request.uuid])} +@app.get("/backup/all", dependencies=[Depends(verify_api_key)]) +def backup_all(): + return {"message": run_command([f"{BIN_PATH}/backupAll"])} + + @app.get("/backup/borgpush", dependencies=[Depends(verify_api_key)]) def backup_borgpush(): return {"message": run_command(["doas", "-u", "4server", f"{BIN_PATH}/borgpush"])} diff --git a/app/sbin/audit b/app/sbin/audit index 097dff9..d06110d 100755 --- a/app/sbin/audit +++ b/app/sbin/audit @@ -132,16 +132,6 @@ def main(): # Users audit["passwd"] = read_file("/etc/passwd") - # Runtime info - audit["uptime"] = cmd(["uptime"]) - audit["top"] = cmd(["top", "-b", "-n", "1"]).splitlines() - - # cgroups (useful in Docker) - audit["cgroup"] = read_file("/proc/self/cgroup") - - # Mounted filesystems - audit["mounts"] = cmd(["mount"]).splitlines() - print(json.dumps(audit, indent=2)) diff --git a/app/sbin/backup/ODOO b/app/sbin/backup/ODOO index bb1cc7c..16c1742 100755 --- a/app/sbin/backup/ODOO +++ b/app/sbin/backup/ODOO @@ -13,7 +13,7 @@ source /4server/sbin/helpers get_contract_info log "UUID: $UUID" -log "Backup slots: $BACKUP_SLOTS" +log "Retention: daily 14d, weekly 12w" log "DB host: $POSTGRES_HOST" # Build paths @@ -48,13 +48,51 @@ doas docker exec "$UUID" chown odoo:odoo -R /mnt/backup doas ln -sf "../$FILENAME" "$BB_DIR/current.zip" log "bb/current.zip -> ../$FILENAME" -# Remove old backups beyond the configured slot count -# Process substitution (< <(...)) keeps the loop in the main shell so -# set -e / exit codes propagate correctly — unlike a pipe subshell. -while IFS= read -r old_file; do - log "Deleting old backup: $old_file" - doas rm -f "$old_file" -done < <(ls -t "$BACKUP_DIR"/[0-9]*.zip 2>/dev/null | tail -n +$((BACKUP_SLOTS + 3))) +# --- Backup retention --- +# Policy: +# - Keep all backups from the last 14 days (daily tier) +# - For backups older than 14 days but within 3 months (12 weeks), +# keep one per ISO week (the newest in each week) +# - Delete everything else +BACKUP_DAYS_DAILY=14 +BACKUP_WEEKS_WEEKLY=12 # ~3 months + +# Map of kept files (absolute path -> 1) +declare -A kept_files +# Track which ISO weeks already have a kept weekly backup +declare -A weekly_kept + +now_ts=$(date +%s) + +while IFS= read -r file; do + filename=$(basename "$file") + filedate="${filename:0:8}" # YYYYMMDD from filename + # BusyBox date (Alpine) requires YYYY-MM-DD; compact YYYYMMDD is rejected + filedate_fmt="${filedate:0:4}-${filedate:4:2}-${filedate:6:2}" + file_ts=$(date -d "$filedate_fmt" +%s 2>/dev/null) || continue + age_days=$(( (now_ts - file_ts) / 86400 )) + + if [ "$age_days" -le "$BACKUP_DAYS_DAILY" ]; then + # --- Daily tier: keep everything within the last 14 days --- + kept_files["$file"]=1 + elif [ "$age_days" -le $((BACKUP_DAYS_DAILY + BACKUP_WEEKS_WEEKLY * 7)) ]; then + # --- Weekly tier: keep 1 per ISO week --- + week_key=$(date -d "$filedate_fmt" +%G%V) # ISO year + week number + if [ -z "${weekly_kept[$week_key]-}" ]; then + weekly_kept[$week_key]=1 + kept_files["$file"]=1 + fi + fi +done < <(ls -t "$BACKUP_DIR"/[0-9]*.zip 2>/dev/null) + +# Delete any backup not in the keep map +for file in "$BACKUP_DIR"/[0-9]*.zip; do + [ -f "$file" ] || continue + if [ -z "${kept_files[$file]-}" ]; then + log "Deleting old backup: $file" + doas rm -f "$file" + fi +done log "Backup finished successfully ." diff --git a/app/sbin/checkBackup b/app/sbin/checkBackup index a0c6b0d..7ed8571 100755 --- a/app/sbin/checkBackup +++ b/app/sbin/checkBackup @@ -17,6 +17,7 @@ if not directory.is_dir(): sys.exit(1) latest_date = None +latest_file = None for zip_file in directory.rglob("*.zip"): name = zip_file.name @@ -29,10 +30,15 @@ for zip_file in directory.rglob("*.zip"): 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_file.stat().st_size < 1000: + print("01-01-2000") + sys.exit(0) + # Format as yyyy-mm-dd print(f"{latest_date[:4]}-{latest_date[4:6]}-{latest_date[6:8]}") diff --git a/app/sbin/stats b/app/sbin/stats new file mode 100755 index 0000000..3a26351 --- /dev/null +++ b/app/sbin/stats @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +"""Collect server runtime stats (uptime, top, cgroup, mounts) and return as JSON.""" + +import json +import subprocess + + +def cmd(args, splitlines=False): + try: + result = subprocess.run( + args, + capture_output=True, + text=True, + timeout=30, + ) + out = (result.stdout + result.stderr).strip() + + if splitlines: + return out.splitlines() + + return out + + except Exception as e: + return f"ERROR: {e}" + + +def read_file(path): + try: + with open(path) as f: + return f.read().strip().splitlines() + except FileNotFoundError: + return None + + +def main(): + stats = {} + + stats["uptime"] = cmd(["uptime"]) + stats["top"] = cmd(["sh", "-c", "top -b -n 1 | head -n 3"], splitlines=True) + stats["cgroup"] = read_file("/proc/self/cgroup") + stats["mounts"] = cmd(["mount"], splitlines=True) + + print(json.dumps(stats, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/app/vault/host_vars.img b/app/vault/host_vars.img index 50d725d..46e0060 100644 Binary files a/app/vault/host_vars.img and b/app/vault/host_vars.img differ