Sync from main server - 2026-05-16 00:38:48
This commit is contained in:
@@ -74,7 +74,7 @@ def get_vm_backups():
|
|||||||
f"-o StrictHostKeyChecking=no -o ConnectTimeout=10 "
|
f"-o StrictHostKeyChecking=no -o ConnectTimeout=10 "
|
||||||
f"-o BatchMode=yes "
|
f"-o BatchMode=yes "
|
||||||
f"{VM_USER}@{VM_HOST} "
|
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)
|
stdout, _ = _run(cmd, timeout=25)
|
||||||
if stdout:
|
if stdout:
|
||||||
@@ -85,7 +85,7 @@ def get_vm_backups():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[backups] VM backup fetch error: {e}")
|
print(f"[backups] VM backup fetch error: {e}")
|
||||||
else:
|
else:
|
||||||
backup_dir = '/backups/main-server'
|
backup_dir = '/backups/cloudproject'
|
||||||
if os.path.exists(backup_dir):
|
if os.path.exists(backup_dir):
|
||||||
files = glob.glob(f'{backup_dir}/myapps-backup-*.tar.gz')
|
files = glob.glob(f'{backup_dir}/myapps-backup-*.tar.gz')
|
||||||
files.sort(key=os.path.getmtime, reverse=True)
|
files.sort(key=os.path.getmtime, reverse=True)
|
||||||
@@ -109,7 +109,7 @@ def audit_backup(backup_file, source='local'):
|
|||||||
if source == 'local':
|
if source == 'local':
|
||||||
archive_path = f"/root/backups/{backup_file}"
|
archive_path = f"/root/backups/{backup_file}"
|
||||||
else:
|
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':
|
if not RUNNING_ON_MAIN_SERVER and source == 'local':
|
||||||
tmp_path = f"/tmp/audit_{backup_file}"
|
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'
|
return True, f'Deleted {backup_file} from main server'
|
||||||
|
|
||||||
elif source == 'vm':
|
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 RUNNING_ON_MAIN_SERVER:
|
||||||
if not os.path.exists(archive_path):
|
if not os.path.exists(archive_path):
|
||||||
return False, f'File not found: {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 StrictHostKeyChecking=no -o ConnectTimeout=10 "
|
||||||
f"-o BatchMode=yes "
|
f"-o BatchMode=yes "
|
||||||
f"{VM_USER}@{VM_HOST} "
|
f"{VM_USER}@{VM_HOST} "
|
||||||
f"'rm -f /backups/main-server/{backup_file} "
|
f"'rm -f /backups/cloudproject/{backup_file} "
|
||||||
f"/backups/main-server/{backup_file}.sha256'"
|
f"/backups/cloudproject/{backup_file}.sha256'"
|
||||||
)
|
)
|
||||||
out, err = _run(cmd, timeout=30)
|
out, err = _run(cmd, timeout=30)
|
||||||
if err and 'No such file' not in err:
|
if err and 'No such file' not in err:
|
||||||
|
|||||||
@@ -81,7 +81,6 @@ def r2_test_connection() -> dict:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"success": False, "message": str(e), "bucket_exists": False}
|
return {"success": False, "message": str(e), "bucket_exists": False}
|
||||||
|
|
||||||
|
|
||||||
def r2_ensure_bucket() -> tuple[bool, str]:
|
def r2_ensure_bucket() -> tuple[bool, str]:
|
||||||
cfg = _get_r2_config()
|
cfg = _get_r2_config()
|
||||||
bucket = cfg["bucket_name"]
|
bucket = cfg["bucket_name"]
|
||||||
@@ -125,6 +124,35 @@ def r2_list_backups() -> list[dict]:
|
|||||||
except Exception:
|
except Exception:
|
||||||
return []
|
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]:
|
def r2_delete_backup(key: str) -> tuple[bool, str]:
|
||||||
cfg = _get_r2_config()
|
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(
|
_upload_jobs[job_id]["log"].append(
|
||||||
f"Upload complete in {elapsed}s — r2://{bucket}/{object_key}"
|
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]["status"] = "done"
|
||||||
_upload_jobs[job_id]["progress"] = 100
|
_upload_jobs[job_id]["progress"] = 100
|
||||||
_upload_jobs[job_id]["object_key"] = object_key
|
_upload_jobs[job_id]["object_key"] = object_key
|
||||||
|
|||||||
1
platform/requirements.txt
Normal file
1
platform/requirements.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
boto3==1.43.5
|
||||||
@@ -3,49 +3,62 @@
|
|||||||
# backup-myapps.sh — Run on MAIN SERVER
|
# backup-myapps.sh — Run on MAIN SERVER
|
||||||
# Backs up: Frappe, Nextcloud, Mautic, n8n, Odoo
|
# Backs up: Frappe, Nextcloud, Mautic, n8n, Odoo
|
||||||
# Storage tiers:
|
# Storage tiers:
|
||||||
# 1. Local → /root/backups/
|
# 1. Local → /root/backups/ (CRITICAL — always runs)
|
||||||
# 2. VM → SSH tunnel → /backups/main-server/
|
# 2. VM → SSH tunnel → /backups/main-server/
|
||||||
# 3. Cloud → Cloudflare R2 (S3-compatible)
|
# 3. Cloud → Cloudflare R2 (S3-compatible, optional)
|
||||||
# Usage: ./backup-myapps.sh
|
#
|
||||||
|
# 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_DATE=$(date +%Y%m%d_%H%M%S)
|
||||||
BACKUP_NAME="myapps-backup-${BACKUP_DATE}"
|
BACKUP_NAME="myapps-backup-${BACKUP_DATE}"
|
||||||
BACKUP_DIR="/root/backups/${BACKUP_NAME}"
|
BACKUP_DIR="/root/backups/${BACKUP_NAME}"
|
||||||
BACKUP_ARCHIVE="/root/backups/${BACKUP_NAME}.tar.gz"
|
BACKUP_ARCHIVE="/root/backups/${BACKUP_NAME}.tar.gz"
|
||||||
|
|
||||||
# SSH config to reach the VM (backup destination)
|
|
||||||
VM_USER="root"
|
VM_USER="root"
|
||||||
VM_HOST="localhost"
|
VM_HOST="178.18.243.51"
|
||||||
VM_PORT="2223"
|
VM_PORT="22"
|
||||||
VM_KEY="/root/.ssh/contabo-key"
|
VM_KEY="/root/.ssh/id_rsa"
|
||||||
VM_DEST="/backups/main-server/"
|
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_ACCOUNT_ID="${R2_ACCOUNT_ID:-35e00c230cc8066252a2d9890b69aea2}"
|
||||||
R2_BUCKET="${R2_BUCKET_NAME:-navitrends-backups}"
|
R2_BUCKET="${R2_BUCKET_NAME:-navitrends-backups}"
|
||||||
R2_ENDPOINT="https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com"
|
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"
|
BACKUP_LOG_FILE="/root/backups/backup-status.log"
|
||||||
MAX_BACKUPS=10
|
MAX_BACKUPS=10
|
||||||
|
MAX_R2_BACKUPS=5
|
||||||
|
|
||||||
|
TIER1_STATUS="pending"
|
||||||
|
TIER2_STATUS="pending"
|
||||||
|
TIER3_STATUS="pending"
|
||||||
|
|
||||||
# ── Write status to log ──────────────────────────────────────────────────────
|
|
||||||
log_status() {
|
log_status() {
|
||||||
local status="$1" # SUCCESS or FAILED
|
local status="$1" name="$2" msg="${3:-}"
|
||||||
local name="$2"
|
|
||||||
local msg="${3:-}"
|
|
||||||
echo "$(date '+%Y-%m-%d %H:%M:%S') | ${status} | ${name} | ${msg}" >> "$BACKUP_LOG_FILE"
|
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 " Tiers: Local → VM → ☁ Cloudflare R2"
|
||||||
echo "========================================="
|
echo "========================================="
|
||||||
|
|
||||||
mkdir -p "$BACKUP_DIR"
|
mkdir -p "$BACKUP_DIR" "/root/backups"
|
||||||
mkdir -p "/root/backups"
|
|
||||||
cd "$BACKUP_DIR"
|
cd "$BACKUP_DIR"
|
||||||
|
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
# 1. Docker container list (filtered to your apps)
|
# 1. Docker container list
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
echo ""
|
echo ""
|
||||||
echo "📋 [1/7] Saving Docker container list..."
|
echo "📋 [1/7] Saving Docker container list..."
|
||||||
docker ps -a --filter "name=frappe" \
|
docker ps -a \
|
||||||
--filter "name=nextcloud" \
|
--filter "name=frappe" --filter "name=nextcloud" \
|
||||||
--filter "name=mautic" \
|
--filter "name=mautic" --filter "name=n8n" --filter "name=odoo" \
|
||||||
--filter "name=n8n" \
|
--format "table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}" \
|
||||||
--filter "name=odoo" \
|
|
||||||
--format "table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}" \
|
|
||||||
> docker-containers.txt 2>/dev/null || true
|
> docker-containers.txt 2>/dev/null || true
|
||||||
echo " ✅ Done"
|
echo " ✅ Done"
|
||||||
|
|
||||||
@@ -79,7 +89,6 @@ echo " ✅ Done"
|
|||||||
echo ""
|
echo ""
|
||||||
echo "📄 [2/7] Saving docker-compose files..."
|
echo "📄 [2/7] Saving docker-compose files..."
|
||||||
mkdir -p compose-files
|
mkdir -p compose-files
|
||||||
|
|
||||||
for app in frappe-setup odoo-clean nextcloud-setup mautic-setup n8n-setup; do
|
for app in frappe-setup odoo-clean nextcloud-setup mautic-setup n8n-setup; do
|
||||||
if [ -d ~/$app ]; then
|
if [ -d ~/$app ]; then
|
||||||
cp -r ~/$app compose-files/ && echo " ✅ $app" || echo " ⚠️ $app copy failed"
|
cp -r ~/$app compose-files/ && echo " ✅ $app" || echo " ⚠️ $app copy failed"
|
||||||
@@ -117,9 +126,8 @@ for volume in "${VOLUMES[@]}"; do
|
|||||||
docker run --rm \
|
docker run --rm \
|
||||||
-v "${volume}:/source:ro" \
|
-v "${volume}:/source:ro" \
|
||||||
-v "$(pwd)/volumes:/backup" \
|
-v "$(pwd)/volumes:/backup" \
|
||||||
alpine \
|
alpine tar czf "/backup/${volume}.tar.gz" -C /source . \
|
||||||
tar czf "/backup/${volume}.tar.gz" -C /source . \
|
&& echo "✅" || echo "⚠️ FAILED (continuing)"
|
||||||
&& echo "✅" || echo "⚠️ FAILED"
|
|
||||||
done
|
done
|
||||||
|
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
@@ -130,55 +138,56 @@ echo "🔧 [4/7] Saving container inspect configs..."
|
|||||||
mkdir -p container-configs
|
mkdir -p container-configs
|
||||||
COUNT=0
|
COUNT=0
|
||||||
while IFS= read -r cid; do
|
while IFS= read -r cid; do
|
||||||
name=$(docker inspect --format='{{.Name}}' "$cid" | sed 's/\///')
|
name=$(docker inspect --format='{{.Name}}' "$cid" 2>/dev/null | sed 's/\///')
|
||||||
docker inspect "$cid" > "container-configs/${name}.json" 2>/dev/null && COUNT=$((COUNT+1))
|
if [ -n "$name" ]; then
|
||||||
done < <(docker ps -a --filter "name=frappe" \
|
docker inspect "$cid" > "container-configs/${name}.json" 2>/dev/null \
|
||||||
--filter "name=nextcloud" \
|
&& COUNT=$((COUNT+1)) || true
|
||||||
--filter "name=mautic" \
|
fi
|
||||||
--filter "name=n8n" \
|
done < <(docker ps -a \
|
||||||
--filter "name=odoo" \
|
--filter "name=frappe" --filter "name=nextcloud" \
|
||||||
--format "{{.ID}}")
|
--filter "name=mautic" --filter "name=n8n" --filter "name=odoo" \
|
||||||
|
--format "{{.ID}}" 2>/dev/null || true)
|
||||||
echo " ✅ Saved $COUNT container configs"
|
echo " ✅ Saved $COUNT container configs"
|
||||||
|
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
# 5. Extract important app config files
|
# 5. App config files
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
echo ""
|
echo ""
|
||||||
echo "⚙️ [5/7] Extracting app config files..."
|
echo "⚙️ [5/7] Extracting app config files..."
|
||||||
mkdir -p configs
|
mkdir -p configs
|
||||||
|
|
||||||
docker run --rm \
|
docker run --rm -v nextcloud-setup_nextcloud-data:/source:ro \
|
||||||
-v nextcloud-setup_nextcloud-data:/source:ro \
|
|
||||||
alpine cat /source/config/config.php > configs/nextcloud-config.php 2>/dev/null \
|
alpine cat /source/config/config.php > configs/nextcloud-config.php 2>/dev/null \
|
||||||
&& echo " ✅ Nextcloud config.php" \
|
&& echo " ✅ Nextcloud config.php" \
|
||||||
|| echo " ⏭️ Nextcloud config not found"
|
|| echo " ⏭️ Nextcloud config not found (skipping)"
|
||||||
|
|
||||||
docker exec frappe-erpnext \
|
docker exec frappe-erpnext \
|
||||||
cat /home/frappe/frappe-bench/sites/erpnext.navitrends.ovh/site_config.json \
|
cat /home/frappe/frappe-bench/sites/erpnext.navitrends.ovh/site_config.json \
|
||||||
> configs/frappe-site_config.json 2>/dev/null \
|
> configs/frappe-site_config.json 2>/dev/null \
|
||||||
&& echo " ✅ Frappe site_config.json" \
|
&& 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 ""
|
||||||
echo "📝 [6/7] Writing backup metadata..."
|
echo "📝 [6/7] Writing backup metadata..."
|
||||||
VOLUME_COUNT=$(ls volumes/*.tar.gz 2>/dev/null | wc -l)
|
VOLUME_COUNT=$(ls volumes/*.tar.gz 2>/dev/null | wc -l)
|
||||||
|
|
||||||
cat > backup-info.txt << EOF
|
cat > backup-info.txt << EOF
|
||||||
Backup Name: $BACKUP_NAME
|
Backup Name: $BACKUP_NAME
|
||||||
Backup Date: $(date)
|
Backup Date: $(date)
|
||||||
Hostname: $(hostname)
|
Hostname: $(hostname)
|
||||||
Apps: Frappe, Nextcloud, Mautic, n8n, Odoo
|
Apps: Frappe, Nextcloud, Mautic, n8n, Odoo
|
||||||
Volumes: $VOLUME_COUNT volume(s) backed up
|
Volumes: $VOLUME_COUNT volume(s) backed up
|
||||||
Docker info: $(docker --version)
|
Docker info: $(docker --version 2>/dev/null || echo 'N/A')
|
||||||
Storage Tiers:
|
Storage Tiers:
|
||||||
- Local: /root/backups/
|
- Local: /root/backups/
|
||||||
- VM: ${VM_HOST}:${VM_PORT} → ${VM_DEST}
|
- VM: ${VM_HOST}:${VM_PORT} → ${VM_DEST}
|
||||||
- Cloud: Cloudflare R2 → s3://${R2_BUCKET}/backups/
|
- Cloud: Cloudflare R2 → s3://${R2_BUCKET}/backups/
|
||||||
|
|
||||||
Volumes included:
|
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
|
EOF
|
||||||
|
|
||||||
echo "" >> backup-info.txt
|
echo "" >> backup-info.txt
|
||||||
@@ -186,143 +195,204 @@ echo "Volume SHA256 checksums:" >> backup-info.txt
|
|||||||
for f in volumes/*.tar.gz; do
|
for f in volumes/*.tar.gz; do
|
||||||
[ -f "$f" ] && sha256sum "$f" | awk '{print $1 " " $2}' >> backup-info.txt || true
|
[ -f "$f" ] && sha256sum "$f" | awk '{print $1 " " $2}' >> backup-info.txt || true
|
||||||
done
|
done
|
||||||
|
|
||||||
echo " ✅ Done"
|
echo " ✅ Done"
|
||||||
|
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
# 7. Compress the backup
|
# 7. Compress
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
echo ""
|
echo ""
|
||||||
echo "🗜️ [7/7] Compressing backup..."
|
echo "🗜️ [7/7] Compressing backup..."
|
||||||
cd /root/backups
|
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)
|
COMPRESSED_SIZE=$(du -h "${BACKUP_NAME}.tar.gz" | cut -f1)
|
||||||
echo " ✅ Compressed size: $COMPRESSED_SIZE → $BACKUP_ARCHIVE"
|
echo " ✅ Compressed size: $COMPRESSED_SIZE → $BACKUP_ARCHIVE"
|
||||||
|
|
||||||
sha256sum "${BACKUP_NAME}.tar.gz" > "${BACKUP_NAME}.tar.gz.sha256"
|
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"
|
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 ""
|
||||||
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_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
|
if [ "$ARCHIVE_COUNT" -gt "$MAX_BACKUPS" ]; then
|
||||||
TO_DELETE=$(echo "$ARCHIVE_LIST" | tail -n +$((MAX_BACKUPS + 1)))
|
TO_DELETE=$(echo "$ARCHIVE_LIST" | tail -n +$((MAX_BACKUPS + 1)))
|
||||||
while IFS= read -r old_file; do
|
while IFS= read -r old_file; do
|
||||||
[ -z "$old_file" ] && continue
|
[ -z "$old_file" ] && continue
|
||||||
rm -f "$old_file"
|
rm -f "$old_file" "${old_file}.sha256"
|
||||||
rm -f "${old_file}.sha256"
|
echo " 🗑️ Deleted: $(basename $old_file)"
|
||||||
echo " 🗑️ Deleted old backup: $(basename $old_file)"
|
|
||||||
done <<< "$TO_DELETE"
|
done <<< "$TO_DELETE"
|
||||||
else
|
else
|
||||||
echo " ✅ ${ARCHIVE_COUNT}/${MAX_BACKUPS} backups — nothing to prune"
|
echo " ✅ ${ARCHIVE_COUNT}/${MAX_BACKUPS} backups — nothing to prune"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
# 9. Send to VM over SSH [TIER 2]
|
# 9. VM transfer [TIER 2] — non-fatal
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
echo ""
|
echo ""
|
||||||
echo "📤 [TIER 2] Sending backup to VM (${VM_HOST}:${VM_PORT})..."
|
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!"
|
echo " ✅ Backup sent to VM successfully!"
|
||||||
scp -i "$VM_KEY" -P "$VM_PORT" -o StrictHostKeyChecking=no \
|
scp -i "$VM_KEY" -P "$VM_PORT" \
|
||||||
"${BACKUP_NAME}.tar.gz.sha256" "${VM_USER}@${VM_HOST}:${VM_DEST}" 2>/dev/null || true
|
-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
|
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
|
fi
|
||||||
|
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
# 10. Upload to Cloudflare R2 [TIER 3 — CLOUD]
|
# 10. Cloudflare R2 [TIER 3] — non-fatal
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
echo ""
|
echo ""
|
||||||
echo "☁️ [TIER 3] Uploading to Cloudflare R2..."
|
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 [ -z "${AWS_ACCESS_KEY_ID:-}" ] || [ -z "${AWS_SECRET_ACCESS_KEY:-}" ]; then
|
||||||
if command -v aws &>/dev/null && [ -n "${AWS_ACCESS_KEY_ID:-}" ] && [ -n "${AWS_SECRET_ACCESS_KEY:-}" ]; then
|
echo " ⚠️ R2 credentials not found — skipping cloud upload"
|
||||||
# Upload main archive
|
echo " 💡 Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in /root/.r2-credentials"
|
||||||
aws s3 cp "${BACKUP_NAME}.tar.gz" \
|
TIER3_STATUS="skipped:no_credentials"
|
||||||
"s3://${R2_BUCKET}/backups/${BACKUP_NAME}.tar.gz" \
|
else
|
||||||
--endpoint-url "${R2_ENDPOINT}" \
|
echo " Credentials: found (key=${AWS_ACCESS_KEY_ID:0:8}...)"
|
||||||
--no-progress \
|
echo " Bucket: ${R2_BUCKET}"
|
||||||
&& echo " ✅ R2 upload complete: s3://${R2_BUCKET}/backups/${BACKUP_NAME}.tar.gz" \
|
echo " Endpoint: ${R2_ENDPOINT}"
|
||||||
|| echo " ❌ R2 upload failed"
|
|
||||||
|
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:
|
try:
|
||||||
import boto3
|
|
||||||
client = boto3.client(
|
client = boto3.client(
|
||||||
's3',
|
's3',
|
||||||
endpoint_url='${R2_ENDPOINT}',
|
endpoint_url=endpoint,
|
||||||
aws_access_key_id=os.environ.get('AWS_ACCESS_KEY_ID',''),
|
aws_access_key_id=access_key,
|
||||||
aws_secret_access_key=os.environ.get('AWS_SECRET_ACCESS_KEY',''),
|
aws_secret_access_key=secret_key,
|
||||||
region_name='auto'
|
region_name='auto'
|
||||||
)
|
)
|
||||||
archive = '/root/backups/${BACKUP_NAME}.tar.gz'
|
|
||||||
key = 'backups/${BACKUP_NAME}.tar.gz'
|
# Ensure bucket exists
|
||||||
size = os.path.getsize(archive)
|
try:
|
||||||
print(f' Uploading {size/1024/1024:.1f} MB...')
|
client.head_bucket(Bucket=bucket)
|
||||||
client.upload_file(archive, '${R2_BUCKET}', key)
|
except Exception:
|
||||||
print(' ✅ R2 upload complete (boto3 fallback)')
|
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:
|
except Exception as e:
|
||||||
print(f' ❌ R2 upload failed: {e}')
|
print(f' ERROR: {e}')
|
||||||
sys.exit(1)
|
sys.exit(2)
|
||||||
PYEOF
|
PYEOF
|
||||||
R2_STATUS="uploaded via boto3"
|
|
||||||
else
|
"$PYTHON_BIN" "$R2_SCRIPT"
|
||||||
echo " ⚠️ Skipping R2 upload: neither aws-cli nor python3/boto3 available"
|
R2_RC=$?
|
||||||
echo " 💡 Install: pip install boto3 OR apt install awscli"
|
rm -f "$R2_SCRIPT"
|
||||||
echo " 💡 Then set: export AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=..."
|
|
||||||
R2_STATUS="skipped (no cli available)"
|
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
|
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 "========================================="
|
echo "========================================="
|
||||||
echo "✅ BACKUP COMPLETE — 3-tier storage"
|
echo "✅ BACKUP COMPLETE"
|
||||||
echo ""
|
echo ""
|
||||||
echo " 📦 Name: $BACKUP_NAME"
|
echo " 📦 Name: $BACKUP_NAME"
|
||||||
echo " 💾 Local: $BACKUP_ARCHIVE ($COMPRESSED_SIZE)"
|
echo " 💾 Local: $BACKUP_ARCHIVE ($COMPRESSED_SIZE)"
|
||||||
echo " 🖥️ VM: ${VM_HOST}:${VM_DEST}${BACKUP_NAME}.tar.gz"
|
echo " 🖥️ VM: ${TIER2_STATUS}"
|
||||||
echo " ☁️ R2: s3://${R2_BUCKET}/backups/${BACKUP_NAME}.tar.gz"
|
echo " ☁️ R2: ${TIER3_STATUS}"
|
||||||
echo "========================================="
|
echo "========================================="
|
||||||
|
|
||||||
# ── AES-256 encryption (optional, call manually) ─────────────────────────────
|
log_status "SUCCESS" "$BACKUP_NAME" \
|
||||||
encrypt_backup() {
|
"size=${COMPRESSED_SIZE} | tier1=${TIER1_STATUS} | tier2=${TIER2_STATUS} | tier3=${TIER3_STATUS}"
|
||||||
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"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
# ── Notification on failure (optional) ───────────────────────────────────────
|
||||||
notify_failure() {
|
notify_failure() {
|
||||||
echo "📧 Envoi notification échec..."
|
|
||||||
echo "Backup FAILED: $BACKUP_NAME" | \
|
echo "Backup FAILED: $BACKUP_NAME" | \
|
||||||
mail -s "[Navitrends] BACKUP FAILED - $(date)" ameniboukottaya@gmail.com
|
mail -s "[Navitrends] BACKUP FAILED - $(date)" ameniboukottaya@gmail.com
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user