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:
2026-10-10 16:53:10 +03:30
parent 19072ee534
commit a9cccc6550
22 changed files with 374 additions and 447 deletions

View File

@@ -0,0 +1,40 @@
"""
وضعیتِ کپیِ زنده‌ی دیتابیس در /api/health (config/health.py: _slot_state، _offsite_warnings).
پرسشِ واقعی از pg_replication_slots این‌جا اجرا نمی‌شود (تست‌ها روی SQLite‌اند)؛ روی سرور با اسلاتِ
واقعی دیده شد (docs/operations.md «بکاپ»).
"""
from django.test import SimpleTestCase, override_settings
from config import health
class SlotStateTests(SimpleTestCase):
def test_missing_slot(self):
self.assertEqual(health._slot_state(None)['state'], 'slot_missing')
def test_streaming_and_caught_up_is_ok(self):
self.assertEqual(health._slot_state((True, 'reserved', 0)), {'state': 'ok', 'lagBytes': 0})
def test_inactive_slot_is_disconnected(self):
self.assertEqual(health._slot_state((False, 'reserved', 0))['state'], 'disconnected')
def test_lost_slot_wins_over_everything(self):
self.assertEqual(health._slot_state((False, 'lost', None))['state'], 'lost')
@override_settings(OFFSITE_WAL_MAX_LAG_BYTES=100)
def test_lag_above_limit(self):
self.assertEqual(health._slot_state((True, 'reserved', 101))['state'], 'lagging')
self.assertEqual(health._slot_state((True, 'reserved', 100))['state'], 'ok')
class OffsiteWarningTests(SimpleTestCase):
def test_ok_and_dev_are_silent(self):
self.assertEqual(health._offsite_warnings({'state': 'ok', 'lagBytes': 0}), [])
self.assertEqual(health._offsite_warnings({'state': 'not_postgres'}), [])
def test_every_bad_state_warns(self):
for state in ('not_configured', 'slot_missing', 'disconnected', 'lagging', 'lost', 'error'):
self.assertEqual(len(health._offsite_warnings({'state': state})), 1, state)
def test_sqlite_reports_not_postgres(self):
self.assertEqual(health._offsite_backup(), {'state': 'not_postgres'})