Live off-site Postgres copy on 247 replaces the nightly pg_dump; backup retention in deploy.sh
- ops/pgbackup: SSH tunnel + pg_receivewal (slot zareian_247) + daily verified pg_basebackup (keep 7) and a restore check, all run on 78.38.35.247 as systemd units - /api/health reports offsiteBackup (slot active/lag/lost) and warns; OFFSITE_WAL_SLOT setting - removed statuslog backup_db/backup_verify and their health warnings - deploy.sh keeps 3 pre-deploy dumps, 3 local patches, drops manual backups after 30 days - docs: operations backup section, security item 14 closed, CLAUDE.md status Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -144,41 +144,62 @@ def _ai_provider_state() -> dict:
|
||||
return {'state': row['new_status'], 'since': row['created_at'].isoformat()}
|
||||
|
||||
|
||||
def _backup_warnings() -> list[str]:
|
||||
"""بکاپِ روزانه، کپیِ بیرونی و آزمونِ بازیابی؛ فقط وقتی Postgres است (SQLite یعنی توسعهی محلی)."""
|
||||
from datetime import datetime, timezone as dt_tz
|
||||
def _offsite_backup() -> dict:
|
||||
"""
|
||||
کپیِ زندهی دیتابیس روی سرورِ دیگر (`ops/pgbackup/`، شرح در docs/operations.md «بکاپ»).
|
||||
|
||||
from statuslog.backup import read_status
|
||||
آن سرور با `pg_receivewal` از اسلاتِ تکثیرِ `OFFSITE_WAL_SLOT` هر تغییر را لحظهای میگیرد؛ اینجا از
|
||||
سمتِ همین دیتابیس دیده میشود که وصل است و چقدر عقب است. ستنبودنِ اسلات روی Postgres یعنی کپیِ
|
||||
بیرونی نداریم و هشدار میشود. base backupِ روزانهی آن سرور از اینجا دیده نمیشود (journalِ همانجا).
|
||||
"""
|
||||
if connection.vendor != 'postgresql':
|
||||
return {'state': 'not_postgres'}
|
||||
slot = settings.OFFSITE_WAL_SLOT
|
||||
if not slot:
|
||||
return {'state': 'not_configured'}
|
||||
try:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
'SELECT active, wal_status, pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) '
|
||||
'FROM pg_replication_slots WHERE slot_name = %s',
|
||||
[slot],
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return {'state': 'error', 'error': type(exc).__name__}
|
||||
return _slot_state(row)
|
||||
|
||||
if settings.DEBUG or connection.vendor != 'postgresql':
|
||||
return []
|
||||
st = read_status()
|
||||
now = datetime.now(dt_tz.utc)
|
||||
|
||||
def age_hours(iso: str):
|
||||
return (now - datetime.fromisoformat(iso)).total_seconds() / 3600 if iso else None
|
||||
def _slot_state(row) -> dict:
|
||||
if row is None:
|
||||
return {'state': 'slot_missing'}
|
||||
active, wal_status, lag = row
|
||||
lag = int(lag or 0)
|
||||
if wal_status == 'lost':
|
||||
# سقفِ max_slot_wal_keep_size خورده: دیسکِ این سرور امن ماند ولی زنجیرهی WAL آنجا بریده است.
|
||||
state = 'lost'
|
||||
elif not active:
|
||||
state = 'disconnected'
|
||||
elif lag > settings.OFFSITE_WAL_MAX_LAG_BYTES:
|
||||
state = 'lagging'
|
||||
else:
|
||||
state = 'ok'
|
||||
return {'state': state, 'lagBytes': lag}
|
||||
|
||||
out = []
|
||||
if st.get('corrupt'):
|
||||
return ['status.json بکاپ خوانده نشد؛ لاگ را ببینید.']
|
||||
age = age_hours(st.get('last_ok_at'))
|
||||
if age is None:
|
||||
out.append('هیچ بکاپِ زمانبندیشدهای ثبت نشده (manage.py backup_db با cron اجرا نمیشود).')
|
||||
elif age > settings.BACKUP_MAX_AGE_HOURS:
|
||||
out.append(f'آخرین بکاپِ موفق {age:.0f} ساعت پیش بود (سقف {settings.BACKUP_MAX_AGE_HOURS}).')
|
||||
if st.get('last_error'):
|
||||
out.append(f"آخرین تلاشِ بکاپ شکست خورد: {st['last_error'][:200]}")
|
||||
if (st.get('offsite') or {}).get('state') != 'ok':
|
||||
out.append('کپیِ بکاپ بیرون از این ماشین نیست یا شکست خورده (BACKUP_OFFSITE_CMD).')
|
||||
rc = st.get('restore_check') or {}
|
||||
rc_age = age_hours(rc.get('at'))
|
||||
if rc_age is None:
|
||||
out.append('بازیابیِ بکاپ هنوز آزموده نشده (manage.py backup_verify).')
|
||||
elif not rc.get('ok'):
|
||||
out.append('آخرین آزمونِ بازیابی ناهمخوان بود.')
|
||||
elif rc_age > settings.RESTORE_CHECK_MAX_AGE_DAYS * 24:
|
||||
out.append(f'آخرین آزمونِ بازیابی {rc_age / 24:.0f} روز پیش بود.')
|
||||
return out
|
||||
|
||||
_OFFSITE_MESSAGES = {
|
||||
'not_configured': 'کپیِ زندهی دیتابیس بیرون از این سرور تنظیم نشده (OFFSITE_WAL_SLOT).',
|
||||
'slot_missing': 'اسلاتِ تکثیرِ OFFSITE_WAL_SLOT در Postgres نیست؛ سرورِ بکاپ هیچوقت وصل نشده یا اسلات حذف شده.',
|
||||
'disconnected': 'سرورِ بکاپ الان وصل نیست؛ تغییرات روی همین سرور جمع میشوند تا سقفِ max_slot_wal_keep_size.',
|
||||
'lagging': 'سرورِ بکاپ از دیتابیس عقب است.',
|
||||
'lost': 'سرورِ بکاپ آنقدر قطع بود که زنجیرهی WAL برید؛ روی آن سرور اسلات و base backupِ تازه لازم است.',
|
||||
'error': 'وضعیتِ کپیِ زندهی دیتابیس خوانده نشد.',
|
||||
}
|
||||
|
||||
|
||||
def _offsite_warnings(offsite: dict) -> list[str]:
|
||||
msg = _OFFSITE_MESSAGES.get(offsite['state'])
|
||||
return [msg] if msg else []
|
||||
|
||||
|
||||
def _warnings() -> list[str]:
|
||||
@@ -219,7 +240,6 @@ def _warnings() -> list[str]:
|
||||
'اعتبارِ سرویسِ هوش مصنوعی (GapGPT) تمام شده: دستیار برای همهی کاربرها '
|
||||
'«در دسترس نیست» میدهد تا حساب شارژ شود.'
|
||||
)
|
||||
out.extend(_backup_warnings())
|
||||
backend = settings.CACHES['default']['BACKEND']
|
||||
if not settings.DEBUG and 'LocMem' in backend:
|
||||
out.append('کش محلیِ هر پروسه است؛ محدودیتِ نرخ بینِ workerها مشترک نیست.')
|
||||
@@ -261,7 +281,8 @@ class HealthView(APIView):
|
||||
if not HasInternalServiceKey().has_permission(request, self):
|
||||
return Response({'status': 'ok' if critical_ok else 'error'}, status=status)
|
||||
|
||||
warnings = _warnings()
|
||||
offsite = _offsite_backup()
|
||||
warnings = _warnings() + _offsite_warnings(offsite)
|
||||
payload = {
|
||||
'status': 'ok' if critical_ok else 'error',
|
||||
'debug': bool(settings.DEBUG),
|
||||
@@ -269,6 +290,7 @@ class HealthView(APIView):
|
||||
'checks': checks,
|
||||
'integrations': _check_integrations(),
|
||||
'traffic': _traffic(),
|
||||
'offsiteBackup': offsite,
|
||||
'allowedOrigins': list(getattr(settings, 'CORS_ALLOWED_ORIGINS', [])),
|
||||
# IPای که throttle برای همین درخواست میبیند. باید IPِ عمومیِ خودِ
|
||||
# صدازننده باشد؛ اگر IPِ ArvanCloud یا nginx بود، NUM_PROXIES غلط
|
||||
|
||||
Reference in New Issue
Block a user