diff --git a/platform/modules/backups.py b/platform/modules/backups.py index bd84323..2f4ab89 100644 --- a/platform/modules/backups.py +++ b/platform/modules/backups.py @@ -74,7 +74,7 @@ def get_vm_backups(): f"-o StrictHostKeyChecking=no -o ConnectTimeout=10 " f"-o BatchMode=yes " f"{VM_USER}@{VM_HOST} " - f"'ls -t /backups/main-server/myapps-backup-*.tar.gz 2>/dev/null | head -20'" + f"'ls -t /backups/cloudproject/myapps-backup-*.tar.gz 2>/dev/null | head -20'" ) stdout, _ = _run(cmd, timeout=25) if stdout: @@ -85,7 +85,7 @@ def get_vm_backups(): except Exception as e: print(f"[backups] VM backup fetch error: {e}") else: - backup_dir = '/backups/main-server' + backup_dir = '/backups/cloudproject' if os.path.exists(backup_dir): files = glob.glob(f'{backup_dir}/myapps-backup-*.tar.gz') files.sort(key=os.path.getmtime, reverse=True) @@ -109,7 +109,7 @@ def audit_backup(backup_file, source='local'): if source == 'local': archive_path = f"/root/backups/{backup_file}" else: - archive_path = f"/backups/main-server/{backup_file}" + archive_path = f"/backups/cloudproject/{backup_file}" if not RUNNING_ON_MAIN_SERVER and source == 'local': tmp_path = f"/tmp/audit_{backup_file}" @@ -381,7 +381,7 @@ def delete_backup(backup_file, source='local'): return True, f'Deleted {backup_file} from main server' elif source == 'vm': - archive_path = f"/backups/main-server/{backup_file}" + archive_path = f"/backups/cloudproject/{backup_file}" if not RUNNING_ON_MAIN_SERVER: if not os.path.exists(archive_path): return False, f'File not found: {archive_path}' @@ -395,8 +395,8 @@ def delete_backup(backup_file, source='local'): f"-o StrictHostKeyChecking=no -o ConnectTimeout=10 " f"-o BatchMode=yes " f"{VM_USER}@{VM_HOST} " - f"'rm -f /backups/main-server/{backup_file} " - f"/backups/main-server/{backup_file}.sha256'" + f"'rm -f /backups/cloudproject/{backup_file} " + f"/backups/cloudproject/{backup_file}.sha256'" ) out, err = _run(cmd, timeout=30) if err and 'No such file' not in err: diff --git a/platform/modules/cloud_backup.py b/platform/modules/cloud_backup.py index 5a5a2ac..d886104 100644 --- a/platform/modules/cloud_backup.py +++ b/platform/modules/cloud_backup.py @@ -81,7 +81,6 @@ def r2_test_connection() -> dict: except Exception as e: return {"success": False, "message": str(e), "bucket_exists": False} - def r2_ensure_bucket() -> tuple[bool, str]: cfg = _get_r2_config() bucket = cfg["bucket_name"] @@ -125,6 +124,35 @@ def r2_list_backups() -> list[dict]: except Exception: return [] +def r2_enforce_retention(max_backups: int = 5) -> list[str]: + """Keep only the latest max_backups in R2, delete the rest. Returns list of deleted keys.""" + if not r2_is_configured(): + return [] + cfg = _get_r2_config() + bucket = cfg["bucket_name"] + deleted = [] + try: + client = _get_r2_client() + resp = client.list_objects_v2(Bucket=bucket, Prefix="backups/") + # Only count real archives, not .sha256 files + objects = sorted( + [o for o in resp.get("Contents", []) if not o["Key"].endswith(".sha256")], + key=lambda x: x["LastModified"], + reverse=True # newest first + ) + to_delete = objects[max_backups:] # everything beyond the 5 latest + for obj in to_delete: + key = obj["Key"] + client.delete_object(Bucket=bucket, Key=key) + # Also delete the sha256 companion if it exists + try: + client.delete_object(Bucket=bucket, Key=key + ".sha256") + except Exception: + pass + deleted.append(key) + except Exception as e: + print(f"R2 retention error: {e}") + return deleted def r2_delete_backup(key: str) -> tuple[bool, str]: cfg = _get_r2_config() @@ -227,6 +255,10 @@ def r2_upload_async(local_path: str, job_id: str) -> None: _upload_jobs[job_id]["log"].append( f"Upload complete in {elapsed}s β€” r2://{bucket}/{object_key}" ) + # Enforce R2 retention β€” keep only 5 latest + deleted = r2_enforce_retention(max_backups=5) + for dk in deleted: + _upload_jobs[job_id]["log"].append(f"πŸ—‘οΈ Pruned old R2 backup: {dk.replace('backups/', '')}") _upload_jobs[job_id]["status"] = "done" _upload_jobs[job_id]["progress"] = 100 _upload_jobs[job_id]["object_key"] = object_key diff --git a/platform/requirements.txt b/platform/requirements.txt new file mode 100644 index 0000000..9944d97 --- /dev/null +++ b/platform/requirements.txt @@ -0,0 +1 @@ +boto3==1.43.5 diff --git a/scripts/backup-myapps.sh b/scripts/backup-myapps.sh index 66d769c..39592bb 100755 --- a/scripts/backup-myapps.sh +++ b/scripts/backup-myapps.sh @@ -3,49 +3,62 @@ # backup-myapps.sh β€” Run on MAIN SERVER # Backs up: Frappe, Nextcloud, Mautic, n8n, Odoo # Storage tiers: -# 1. Local β†’ /root/backups/ +# 1. Local β†’ /root/backups/ (CRITICAL β€” always runs) # 2. VM β†’ SSH tunnel β†’ /backups/main-server/ -# 3. Cloud β†’ Cloudflare R2 (S3-compatible) -# Usage: ./backup-myapps.sh +# 3. Cloud β†’ Cloudflare R2 (S3-compatible, optional) +# +# NOTE: R2 and VM failures do NOT stop the backup. +# Local backup always completes first. # ============================================= -set -euo pipefail +set -uo pipefail BACKUP_DATE=$(date +%Y%m%d_%H%M%S) BACKUP_NAME="myapps-backup-${BACKUP_DATE}" BACKUP_DIR="/root/backups/${BACKUP_NAME}" BACKUP_ARCHIVE="/root/backups/${BACKUP_NAME}.tar.gz" -# SSH config to reach the VM (backup destination) VM_USER="root" -VM_HOST="localhost" -VM_PORT="2223" -VM_KEY="/root/.ssh/contabo-key" -VM_DEST="/backups/main-server/" +VM_HOST="178.18.243.51" +VM_PORT="22" +VM_KEY="/root/.ssh/id_rsa" +VM_DEST="/backups/cloudproject/" + +# ── Load R2 credentials ─────────────────────────────────────────────────────── +CREDENTIALS_FILE="/root/.r2-credentials" +if [ -f "$CREDENTIALS_FILE" ]; then + set +u + source "$CREDENTIALS_FILE" + set -u +fi + +# Also try config.py as fallback +if [ -z "${AWS_ACCESS_KEY_ID:-}" ]; then + CONFIG_PY="/root/management-platform/config.py" + if [ -f "$CONFIG_PY" ]; then + _KEY=$(grep -oP '(?<=R2_ACCESS_KEY_ID",\s{5}")[^"]+' "$CONFIG_PY" 2>/dev/null || true) + _SEC=$(grep -oP '(?<=R2_SECRET_ACCESS_KEY", ")[^"]+' "$CONFIG_PY" 2>/dev/null || true) + _BKT=$(grep -oP '(?<=R2_BUCKET_NAME",\s{7}")[^"]+' "$CONFIG_PY" 2>/dev/null || true) + [ -n "$_KEY" ] && export AWS_ACCESS_KEY_ID="$_KEY" + [ -n "$_SEC" ] && export AWS_SECRET_ACCESS_KEY="$_SEC" + [ -n "$_BKT" ] && export R2_BUCKET_NAME="$_BKT" + fi +fi -# ── Cloudflare R2 config ───────────────────────────────────────────────────── -# Set these via environment or export before running the script R2_ACCOUNT_ID="${R2_ACCOUNT_ID:-35e00c230cc8066252a2d9890b69aea2}" R2_BUCKET="${R2_BUCKET_NAME:-navitrends-backups}" R2_ENDPOINT="https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com" -# AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY must be exported in the env -# or set in /root/.r2-credentials (sourced below) -CREDENTIALS_FILE="/root/.r2-credentials" -if [ -f "$CREDENTIALS_FILE" ]; then - # shellcheck source=/dev/null - source "$CREDENTIALS_FILE" -fi - -# Log file for backup status (used by boot-check script) BACKUP_LOG_FILE="/root/backups/backup-status.log" MAX_BACKUPS=10 +MAX_R2_BACKUPS=5 + +TIER1_STATUS="pending" +TIER2_STATUS="pending" +TIER3_STATUS="pending" -# ── Write status to log ────────────────────────────────────────────────────── log_status() { - local status="$1" # SUCCESS or FAILED - local name="$2" - local msg="${3:-}" + local status="$1" name="$2" msg="${3:-}" echo "$(date '+%Y-%m-%d %H:%M:%S') | ${status} | ${name} | ${msg}" >> "$BACKUP_LOG_FILE" } @@ -55,21 +68,18 @@ echo " Apps: Frappe, Nextcloud, Mautic, n8n, Odoo" echo " Tiers: Local β†’ VM β†’ ☁ Cloudflare R2" echo "=========================================" -mkdir -p "$BACKUP_DIR" -mkdir -p "/root/backups" +mkdir -p "$BACKUP_DIR" "/root/backups" cd "$BACKUP_DIR" # -------------------------------------------------- -# 1. Docker container list (filtered to your apps) +# 1. Docker container list # -------------------------------------------------- echo "" echo "πŸ“‹ [1/7] Saving Docker container list..." -docker ps -a --filter "name=frappe" \ - --filter "name=nextcloud" \ - --filter "name=mautic" \ - --filter "name=n8n" \ - --filter "name=odoo" \ - --format "table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}" \ +docker ps -a \ + --filter "name=frappe" --filter "name=nextcloud" \ + --filter "name=mautic" --filter "name=n8n" --filter "name=odoo" \ + --format "table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}" \ > docker-containers.txt 2>/dev/null || true echo " βœ… Done" @@ -79,7 +89,6 @@ echo " βœ… Done" echo "" echo "πŸ“„ [2/7] Saving docker-compose files..." mkdir -p compose-files - for app in frappe-setup odoo-clean nextcloud-setup mautic-setup n8n-setup; do if [ -d ~/$app ]; then cp -r ~/$app compose-files/ && echo " βœ… $app" || echo " ⚠️ $app copy failed" @@ -117,9 +126,8 @@ for volume in "${VOLUMES[@]}"; do docker run --rm \ -v "${volume}:/source:ro" \ -v "$(pwd)/volumes:/backup" \ - alpine \ - tar czf "/backup/${volume}.tar.gz" -C /source . \ - && echo "βœ…" || echo "⚠️ FAILED" + alpine tar czf "/backup/${volume}.tar.gz" -C /source . \ + && echo "βœ…" || echo "⚠️ FAILED (continuing)" done # -------------------------------------------------- @@ -130,55 +138,56 @@ echo "πŸ”§ [4/7] Saving container inspect configs..." mkdir -p container-configs COUNT=0 while IFS= read -r cid; do - name=$(docker inspect --format='{{.Name}}' "$cid" | sed 's/\///') - docker inspect "$cid" > "container-configs/${name}.json" 2>/dev/null && COUNT=$((COUNT+1)) -done < <(docker ps -a --filter "name=frappe" \ - --filter "name=nextcloud" \ - --filter "name=mautic" \ - --filter "name=n8n" \ - --filter "name=odoo" \ - --format "{{.ID}}") + name=$(docker inspect --format='{{.Name}}' "$cid" 2>/dev/null | sed 's/\///') + if [ -n "$name" ]; then + docker inspect "$cid" > "container-configs/${name}.json" 2>/dev/null \ + && COUNT=$((COUNT+1)) || true + fi +done < <(docker ps -a \ + --filter "name=frappe" --filter "name=nextcloud" \ + --filter "name=mautic" --filter "name=n8n" --filter "name=odoo" \ + --format "{{.ID}}" 2>/dev/null || true) echo " βœ… Saved $COUNT container configs" # -------------------------------------------------- -# 5. Extract important app config files +# 5. App config files # -------------------------------------------------- echo "" echo "βš™οΈ [5/7] Extracting app config files..." mkdir -p configs -docker run --rm \ - -v nextcloud-setup_nextcloud-data:/source:ro \ +docker run --rm -v nextcloud-setup_nextcloud-data:/source:ro \ alpine cat /source/config/config.php > configs/nextcloud-config.php 2>/dev/null \ && echo " βœ… Nextcloud config.php" \ - || echo " ⏭️ Nextcloud config not found" + || echo " ⏭️ Nextcloud config not found (skipping)" docker exec frappe-erpnext \ cat /home/frappe/frappe-bench/sites/erpnext.navitrends.ovh/site_config.json \ > configs/frappe-site_config.json 2>/dev/null \ && echo " βœ… Frappe site_config.json" \ - || echo " ⏭️ Frappe config not found" + || echo " ⏭️ Frappe config not found (skipping)" # -------------------------------------------------- -# 6. Backup metadata + checksum +# 6. Metadata + checksum # -------------------------------------------------- echo "" echo "πŸ“ [6/7] Writing backup metadata..." VOLUME_COUNT=$(ls volumes/*.tar.gz 2>/dev/null | wc -l) + cat > backup-info.txt << EOF Backup Name: $BACKUP_NAME Backup Date: $(date) Hostname: $(hostname) Apps: Frappe, Nextcloud, Mautic, n8n, Odoo Volumes: $VOLUME_COUNT volume(s) backed up -Docker info: $(docker --version) +Docker info: $(docker --version 2>/dev/null || echo 'N/A') Storage Tiers: - Local: /root/backups/ - VM: ${VM_HOST}:${VM_PORT} β†’ ${VM_DEST} - Cloud: Cloudflare R2 β†’ s3://${R2_BUCKET}/backups/ Volumes included: -$(ls volumes/*.tar.gz 2>/dev/null | xargs -I{} basename {} || echo "none") +$(ls volumes/*.tar.gz 2>/dev/null | xargs -I{} basename {} 2>/dev/null || echo "none") EOF echo "" >> backup-info.txt @@ -186,143 +195,204 @@ echo "Volume SHA256 checksums:" >> backup-info.txt for f in volumes/*.tar.gz; do [ -f "$f" ] && sha256sum "$f" | awk '{print $1 " " $2}' >> backup-info.txt || true done - echo " βœ… Done" # -------------------------------------------------- -# 7. Compress the backup +# 7. Compress # -------------------------------------------------- echo "" echo "πŸ—œοΈ [7/7] Compressing backup..." cd /root/backups -tar -czf "${BACKUP_NAME}.tar.gz" "${BACKUP_NAME}/" + +tar -czf "${BACKUP_NAME}.tar.gz" "${BACKUP_NAME}/" 2>/dev/null +if [ $? -ne 0 ]; then + echo " ❌ CRITICAL: Compression failed!" + log_status "FAILED" "$BACKUP_NAME" "compression_failed" + exit 1 +fi + COMPRESSED_SIZE=$(du -h "${BACKUP_NAME}.tar.gz" | cut -f1) echo " βœ… Compressed size: $COMPRESSED_SIZE β†’ $BACKUP_ARCHIVE" sha256sum "${BACKUP_NAME}.tar.gz" > "${BACKUP_NAME}.tar.gz.sha256" -echo " βœ… Checksum written: ${BACKUP_NAME}.tar.gz.sha256" +echo " βœ… Checksum written" rm -rf "$BACKUP_DIR" +TIER1_STATUS="ok:${COMPRESSED_SIZE}" +echo "" +echo "βœ… [TIER 1] Local backup complete: $BACKUP_ARCHIVE ($COMPRESSED_SIZE)" # -------------------------------------------------- -# 8. Retention β€” keep only the latest MAX_BACKUPS +# 8. Retention β€” keep only MAX_BACKUPS locally # -------------------------------------------------- echo "" -echo "🧹 [Retention] Keeping latest ${MAX_BACKUPS} backups..." +echo "🧹 [Retention] Keeping latest ${MAX_BACKUPS} local backups..." ARCHIVE_LIST=$(ls -t /root/backups/myapps-backup-*.tar.gz 2>/dev/null || true) -ARCHIVE_COUNT=$(echo "$ARCHIVE_LIST" | grep -c '.tar.gz' || true) +ARCHIVE_COUNT=$(echo "$ARCHIVE_LIST" | grep -c '.tar.gz' 2>/dev/null || echo "0") if [ "$ARCHIVE_COUNT" -gt "$MAX_BACKUPS" ]; then TO_DELETE=$(echo "$ARCHIVE_LIST" | tail -n +$((MAX_BACKUPS + 1))) while IFS= read -r old_file; do [ -z "$old_file" ] && continue - rm -f "$old_file" - rm -f "${old_file}.sha256" - echo " πŸ—‘οΈ Deleted old backup: $(basename $old_file)" + rm -f "$old_file" "${old_file}.sha256" + echo " πŸ—‘οΈ Deleted: $(basename $old_file)" done <<< "$TO_DELETE" else echo " βœ… ${ARCHIVE_COUNT}/${MAX_BACKUPS} backups β€” nothing to prune" fi # -------------------------------------------------- -# 9. Send to VM over SSH [TIER 2] +# 9. VM transfer [TIER 2] β€” non-fatal # -------------------------------------------------- echo "" echo "πŸ“€ [TIER 2] Sending backup to VM (${VM_HOST}:${VM_PORT})..." -scp -i "$VM_KEY" \ - -P "$VM_PORT" \ - -o StrictHostKeyChecking=no \ - -o ConnectTimeout=15 \ - "${BACKUP_NAME}.tar.gz" \ - "${VM_USER}@${VM_HOST}:${VM_DEST}" -if [ $? -eq 0 ]; then +scp -i "$VM_KEY" -P "$VM_PORT" \ + -o StrictHostKeyChecking=no -o ConnectTimeout=30 \ + "${BACKUP_NAME}.tar.gz" \ + "${VM_USER}@${VM_HOST}:${VM_DEST}" 2>/dev/null +VM_SCP_RC=$? + +if [ $VM_SCP_RC -eq 0 ]; then echo " βœ… Backup sent to VM successfully!" - scp -i "$VM_KEY" -P "$VM_PORT" -o StrictHostKeyChecking=no \ - "${BACKUP_NAME}.tar.gz.sha256" "${VM_USER}@${VM_HOST}:${VM_DEST}" 2>/dev/null || true + scp -i "$VM_KEY" -P "$VM_PORT" \ + -o StrictHostKeyChecking=no -o ConnectTimeout=15 \ + "${BACKUP_NAME}.tar.gz.sha256" \ + "${VM_USER}@${VM_HOST}:${VM_DEST}" 2>/dev/null || true + TIER2_STATUS="ok" else - echo " ❌ VM transfer failed. Local backup still at: $BACKUP_ARCHIVE" + echo " ⚠️ VM transfer failed (rc=$VM_SCP_RC) β€” local backup is safe, continuing..." + TIER2_STATUS="failed:rc=${VM_SCP_RC}" fi # -------------------------------------------------- -# 10. Upload to Cloudflare R2 [TIER 3 β€” CLOUD] +# 10. Cloudflare R2 [TIER 3] β€” non-fatal # -------------------------------------------------- echo "" echo "☁️ [TIER 3] Uploading to Cloudflare R2..." -echo " Bucket: ${R2_BUCKET}" -echo " Endpoint: ${R2_ENDPOINT}" -# Check if AWS CLI is available (used for S3-compatible uploads) -if command -v aws &>/dev/null && [ -n "${AWS_ACCESS_KEY_ID:-}" ] && [ -n "${AWS_SECRET_ACCESS_KEY:-}" ]; then - # Upload main archive - aws s3 cp "${BACKUP_NAME}.tar.gz" \ - "s3://${R2_BUCKET}/backups/${BACKUP_NAME}.tar.gz" \ - --endpoint-url "${R2_ENDPOINT}" \ - --no-progress \ - && echo " βœ… R2 upload complete: s3://${R2_BUCKET}/backups/${BACKUP_NAME}.tar.gz" \ - || echo " ❌ R2 upload failed" +if [ -z "${AWS_ACCESS_KEY_ID:-}" ] || [ -z "${AWS_SECRET_ACCESS_KEY:-}" ]; then + echo " ⚠️ R2 credentials not found β€” skipping cloud upload" + echo " πŸ’‘ Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in /root/.r2-credentials" + TIER3_STATUS="skipped:no_credentials" +else + echo " Credentials: found (key=${AWS_ACCESS_KEY_ID:0:8}...)" + echo " Bucket: ${R2_BUCKET}" + echo " Endpoint: ${R2_ENDPOINT}" + + PYTHON_BIN="/root/management-platform/venv/bin/python3" + if [ ! -f "$PYTHON_BIN" ]; then + PYTHON_BIN="python3" + fi + + R2_SCRIPT="/tmp/r2_upload_$$.py" + cat > "$R2_SCRIPT" << PYEOF +import sys, os, boto3 +from datetime import datetime, timezone + +account_id = "${R2_ACCOUNT_ID}" +bucket = "${R2_BUCKET}" +endpoint = "https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com" +access_key = "${AWS_ACCESS_KEY_ID}" +secret_key = "${AWS_SECRET_ACCESS_KEY}" +archive = "/root/backups/${BACKUP_NAME}.tar.gz" +sha_file = archive + '.sha256' +key = "backups/${BACKUP_NAME}.tar.gz" +max_r2 = ${MAX_R2_BACKUPS} - # Upload checksum - aws s3 cp "${BACKUP_NAME}.tar.gz.sha256" \ - "s3://${R2_BUCKET}/backups/${BACKUP_NAME}.tar.gz.sha256" \ - --endpoint-url "${R2_ENDPOINT}" \ - --no-prth -os.environ.setdefault('R2_ACCOUNT_ID', '${R2_ACCOUNT_ID}') -os.environ.setdefault('R2_BUCKET_NAME', '${R2_BUCKET}') try: - import boto3 client = boto3.client( 's3', - endpoint_url='${R2_ENDPOINT}', - aws_access_key_id=os.environ.get('AWS_ACCESS_KEY_ID',''), - aws_secret_access_key=os.environ.get('AWS_SECRET_ACCESS_KEY',''), + endpoint_url=endpoint, + aws_access_key_id=access_key, + aws_secret_access_key=secret_key, region_name='auto' ) - archive = '/root/backups/${BACKUP_NAME}.tar.gz' - key = 'backups/${BACKUP_NAME}.tar.gz' - size = os.path.getsize(archive) - print(f' Uploading {size/1024/1024:.1f} MB...') - client.upload_file(archive, '${R2_BUCKET}', key) - print(' βœ… R2 upload complete (boto3 fallback)') + + # Ensure bucket exists + try: + client.head_bucket(Bucket=bucket) + except Exception: + client.create_bucket(Bucket=bucket) + print(f' Created bucket: {bucket}') + + # Upload archive + size_mb = os.path.getsize(archive) / 1024 / 1024 + timestamp = datetime.now(timezone.utc).isoformat() + print(f' Uploading {size_mb:.1f} MB to r2://{bucket}/{key}') + client.upload_file( + archive, bucket, key, + ExtraArgs={"Metadata": { + "uploaded-by": "backup-script", + "uploaded-at": timestamp, + "original-file": archive.split("/")[-1], + }} + ) + print(' βœ… R2 archive uploaded successfully') + + # Upload sha256 companion + if os.path.exists(sha_file): + client.upload_file(sha_file, bucket, key + '.sha256') + print(' βœ… R2 SHA256 checksum uploaded') + + # ── R2 Retention: keep only max_r2 latest backups ────────── + print(f' 🧹 Enforcing R2 retention (max {max_r2})...') + resp = client.list_objects_v2(Bucket=bucket, Prefix='backups/') + objects = sorted( + [o for o in resp.get('Contents', []) if not o['Key'].endswith('.sha256')], + key=lambda x: x['LastModified'], + reverse=True # newest first + ) + to_delete = objects[max_r2:] + if not to_delete: + print(f' βœ… {len(objects)}/{max_r2} R2 backups β€” nothing to prune') + for obj in to_delete: + old_key = obj['Key'] + client.delete_object(Bucket=bucket, Key=old_key) + try: + client.delete_object(Bucket=bucket, Key=old_key + '.sha256') + except Exception: + pass + print(f' πŸ—‘οΈ Deleted from R2: {old_key.replace("backups/", "")}') + + sys.exit(0) + except Exception as e: - print(f' ❌ R2 upload failed: {e}') - sys.exit(1) + print(f' ERROR: {e}') + sys.exit(2) PYEOF - R2_STATUS="uploaded via boto3" -else - echo " ⚠️ Skipping R2 upload: neither aws-cli nor python3/boto3 available" - echo " πŸ’‘ Install: pip install boto3 OR apt install awscli" - echo " πŸ’‘ Then set: export AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=..." - R2_STATUS="skipped (no cli available)" + + "$PYTHON_BIN" "$R2_SCRIPT" + R2_RC=$? + rm -f "$R2_SCRIPT" + + if [ $R2_RC -eq 0 ]; then + echo " βœ… R2 upload + retention complete" + TIER3_STATUS="ok" + else + echo " ⚠️ R2 upload failed (rc=$R2_RC) β€” local backup is safe at: $BACKUP_ARCHIVE" + TIER3_STATUS="failed:rc=${R2_RC}" + fi fi # -------------------------------------------------- -# 11. Write final status to log +# 11. Final summary + log # -------------------------------------------------- -log_status "SUCCESS" "$BACKUP_NAME" "size=${COMPRESSED_SIZE} | r2=${R2_STATUS}" - echo "" echo "=========================================" -echo "βœ… BACKUP COMPLETE β€” 3-tier storage" +echo "βœ… BACKUP COMPLETE" echo "" -echo " πŸ“¦ Name: $BACKUP_NAME" -echo " πŸ’Ύ Local: $BACKUP_ARCHIVE ($COMPRESSED_SIZE)" -echo " πŸ–₯️ VM: ${VM_HOST}:${VM_DEST}${BACKUP_NAME}.tar.gz" -echo " ☁️ R2: s3://${R2_BUCKET}/backups/${BACKUP_NAME}.tar.gz" +echo " πŸ“¦ Name: $BACKUP_NAME" +echo " πŸ’Ύ Local: $BACKUP_ARCHIVE ($COMPRESSED_SIZE)" +echo " πŸ–₯️ VM: ${TIER2_STATUS}" +echo " ☁️ R2: ${TIER3_STATUS}" echo "=========================================" -# ── AES-256 encryption (optional, call manually) ───────────────────────────── -encrypt_backup() { - echo "πŸ” Chiffrement AES-256..." - openssl enc -aes-256-cbc -pbkdf2 -pass pass:Navitrends2024! \ - -in "$BACKUP_ARCHIVE" \ - -out "${BACKUP_ARCHIVE}.enc" - rm -f "$BACKUP_ARCHIVE" - echo "βœ… Archive chiffrΓ©e : ${BACKUP_ARCHIVE}.enc" -} +log_status "SUCCESS" "$BACKUP_NAME" \ + "size=${COMPRESSED_SIZE} | tier1=${TIER1_STATUS} | tier2=${TIER2_STATUS} | tier3=${TIER3_STATUS}" +# ── Notification on failure (optional) ─────────────────────────────────────── notify_failure() { - echo "πŸ“§ Envoi notification Γ©chec..." echo "Backup FAILED: $BACKUP_NAME" | \ mail -s "[Navitrends] BACKUP FAILED - $(date)" ameniboukottaya@gmail.com }