Compare commits
8 Commits
670513b462
...
43e02adf37
| Author | SHA1 | Date | |
|---|---|---|---|
| 43e02adf37 | |||
| e25ffb5575 | |||
| eb03b467d4 | |||
| f2c57c1ad1 | |||
| f6d779255d | |||
| 4cd859b5fa | |||
| dd98f16d12 | |||
| bd329f83ae |
@@ -39,6 +39,9 @@ DB_PORT=5432
|
||||
# اسلاتِ تکثیری که سرورِ بکاپ از آن کپیِ زنده میگیرد (ops/pgbackup/). روی سرور: zareian_247.
|
||||
# خالی روی Postgres = کپیِ بیرونی نداریم و /api/health هشدار میدهد. روی لپتاپ خالی.
|
||||
OFFSITE_WAL_SLOT=
|
||||
# سرورِ اختصاصی (۲۱۶): فایلی که tadrisino-offsite-sync زمانِ آخرین ارسالِ بکاپِ رمزشده به ۲۴۷ را در آن مینویسد.
|
||||
# روی ۲۱۶: /var/lib/tadrisino-backup/offsite-synced-at (بر OFFSITE_WAL_SLOT مقدم است). روی لپتاپ خالی.
|
||||
OFFSITE_SYNC_STATUS_FILE=
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
# ۲) دامنهها — دو آدرس، بقیه از آنها ساخته میشوند
|
||||
|
||||
15
CLAUDE.md
15
CLAUDE.md
@@ -63,10 +63,18 @@ other people's home directories (`aria`, `artina`, `behdad`, `zahra`, `git`, `gi
|
||||
other nginx site. `metabase` and Artina's `celerybeat` crash-loop and eat ~3 of 8 cores; that is a
|
||||
request to the server admin, not something to fix.
|
||||
|
||||
**`78.38.35.247` (backup server)**, same account and password: ours is only `~/zareian-pgbackup/`
|
||||
and the `zareian-pg-*` / `zareian-applog-pull` units. It also runs other teams' services and its
|
||||
**`78.38.35.247` (backup server)**, same account and password: ours is only `~/zareian-pgbackup/` (249's
|
||||
live copy), `~/zareian-pgbackrest-216/` (216's backups, encrypted on 216; 247 never holds the key), our lines in
|
||||
`~/.ssh/authorized_keys` and `~/.pgpass`, `~/.ssh/zareian_*`, and the `zareian-pg-*` / `zareian-applog-pull` units. It also runs other teams' services and its
|
||||
own `postgresql@14-main`, which we never use.
|
||||
|
||||
**`78.38.35.216` (hostname `mathapp`, new dedicated VM, not yet serving users)**, account `zareian` (sudo; the
|
||||
senior and the company owner share `mathapp-admin`, also sudo; no other logins): the whole machine is ours. The app runs as the non-sudo user `mathapp` under `/home/mathapp/`; same unit names as on 249.
|
||||
Both accounts use the password on the `AdminUser-Password` line of the password file (the `Password` line under
|
||||
`NewServer` is older). State and the cut-over order:
|
||||
`docs/operations.md` «سرورِ اختصاصیِ جدید». Until cut-over, 249 stays the live server and every rule below applies
|
||||
there unchanged; on 216, rules 1, 5, 6, 8 and 10 still apply.
|
||||
|
||||
### Rules (not negotiable)
|
||||
|
||||
1. **Read-only first.** Before changing anything, look: `systemctl status`, `ls`, `git -C … log -1`,
|
||||
@@ -114,7 +122,8 @@ session. Take only the password line, everything after the first `: ` (stripping
|
||||
once sent the whole line and SSH refused it):
|
||||
|
||||
```bash
|
||||
sed -n '/^Password/{s/^[^:]*: *//;p;}' ~/Desktop/'رمز سرور.txt' > "$SCRATCH/.pw" && chmod 600 "$SCRATCH/.pw"
|
||||
sed -n '/^Password/{s/^[^:]*: *//;p;q;}' ~/Desktop/'رمز سرور.txt' | tr -d '\r\n' > "$SCRATCH/.pw" && chmod 600 "$SCRATCH/.pw" # 249/247 (first Password line)
|
||||
awk '/^AdminUser-Password/{sub(/^[^:]*: */,"");printf "%s",$0;exit}' ~/Desktop/'رمز سرور.txt' | tr -d '\r\n' > "$SCRATCH/.pwn" && chmod 600 "$SCRATCH/.pwn" # 216
|
||||
sshpass -f "$SCRATCH/.pw" ssh administrator@78.38.35.249 '<command>'
|
||||
sshpass -f "$SCRATCH/.pw" ssh administrator@78.38.35.249 'sudo -S -p "" <command>' < "$SCRATCH/.pw"
|
||||
rm -P "$SCRATCH/.pw" # as soon as the work is done (macOS has no shred); recreate it next time
|
||||
|
||||
@@ -14,6 +14,7 @@ GET /api/health — «آیا این سرور واقعاً سالم است؟»
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from datetime import timedelta
|
||||
|
||||
from django.conf import settings
|
||||
@@ -160,6 +161,8 @@ def _offsite_backup() -> dict:
|
||||
"""
|
||||
if connection.vendor != 'postgresql':
|
||||
return {'state': 'not_postgres'}
|
||||
if settings.OFFSITE_SYNC_STATUS_FILE:
|
||||
return _pushed_backup()
|
||||
slot = settings.OFFSITE_WAL_SLOT
|
||||
if not slot:
|
||||
return {'state': 'not_configured'}
|
||||
@@ -193,6 +196,45 @@ def _slot_state(row) -> dict:
|
||||
return {'state': state, 'lagBytes': lag}
|
||||
|
||||
|
||||
def _pushed_backup() -> dict:
|
||||
"""
|
||||
بکاپِ رمزشدهی ۲۱۶: pgBackRest هر WAL را با archive_command آرشیو میکند (pg_stat_archiver) و
|
||||
`tadrisino-offsite-sync` مخزن را هر دقیقه به ۲۴۷ میفرستد و زمانش را در OFFSITE_SYNC_STATUS_FILE مینویسد.
|
||||
کهنگیِ آخرین آرشیو هشدار نیست: دیتابیسِ بیتغییر WALِ تازهای ندارد که آرشیو شود.
|
||||
"""
|
||||
try:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute('SELECT last_archived_time, last_failed_time FROM pg_stat_archiver')
|
||||
last_archived, last_failed = cursor.fetchone()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return {'state': 'error', 'error': type(exc).__name__}
|
||||
try:
|
||||
with open(settings.OFFSITE_SYNC_STATUS_FILE) as f:
|
||||
synced_at = int(f.read().strip())
|
||||
except FileNotFoundError:
|
||||
synced_at = None
|
||||
except (OSError, ValueError) as exc:
|
||||
return {'state': 'error', 'error': type(exc).__name__}
|
||||
return _pushed_state(last_archived, last_failed, synced_at, time.time())
|
||||
|
||||
|
||||
def _pushed_state(last_archived, last_failed, synced_at, now) -> dict:
|
||||
age = None if synced_at is None else int(now - synced_at)
|
||||
if last_failed is not None and (last_archived is None or last_failed > last_archived):
|
||||
state = 'archive_failing'
|
||||
elif age is None:
|
||||
state = 'never_synced'
|
||||
elif age > settings.OFFSITE_SYNC_MAX_AGE_SECONDS:
|
||||
state = 'sync_stale'
|
||||
else:
|
||||
state = 'ok'
|
||||
return {
|
||||
'state': state,
|
||||
'syncAgeSeconds': age,
|
||||
'lastArchivedAt': last_archived.isoformat() if last_archived else None,
|
||||
}
|
||||
|
||||
|
||||
# اتصالهایی بیرون از poolِ workerها: pg_receivewal و pg_basebackupِ سرورِ بکاپ (۲)، pg_dump و
|
||||
# migrateِ deploy.sh (۲)، فرمانِ مدیریتی یا psqlِ دستی (۲)، و workerی که gunicorn با max_requests
|
||||
# عوض میکند و تا بسته شدنِ قبلی همزمان بالاست (یک pool).
|
||||
@@ -267,6 +309,9 @@ _OFFSITE_MESSAGES = {
|
||||
'lagging': 'سرورِ بکاپ از دیتابیس عقب است.',
|
||||
'lost': 'سرورِ بکاپ آنقدر قطع بود که زنجیرهی WAL برید؛ روی آن سرور اسلات و base backupِ تازه لازم است.',
|
||||
'error': 'وضعیتِ کپیِ زندهی دیتابیس خوانده نشد.',
|
||||
'archive_failing': 'آرشیوِ WAL (pgBackRest) شکست میخورد و تغییراتِ تازه بکاپ نمیشوند: sudo -u postgres pgbackrest --stanza=zareian check',
|
||||
'never_synced': 'بکاپِ رمزشده هنوز هیچوقت به سرورِ بیرونی (۲۴۷) نرسیده (فایلِ OFFSITE_SYNC_STATUS_FILE نیست).',
|
||||
'sync_stale': 'ارسالِ بکاپِ رمزشده به ۲۴۷ عقب افتاده: journalctl -u tadrisino-offsite-sync',
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1078,6 +1078,12 @@ LOG_DIR = os.environ.get('LOG_DIR', '')
|
||||
OFFSITE_WAL_SLOT = os.environ.get('OFFSITE_WAL_SLOT', '')
|
||||
# بیش از این عقبماندن هشدار است. ۶۴ مگ ≈ چهار فایلِ WAL؛ در ترافیکِ عادی عقبماندگی نزدیکِ صفر است.
|
||||
OFFSITE_WAL_MAX_LAG_BYTES = 64 * 2**20
|
||||
# سرورِ اختصاصی (۲۱۶): بکاپِ رمزشدهی pgBackRest؛ `tadrisino-offsite-sync` هر دقیقه آن را به ۲۴۷ میفرستد و زمانِ
|
||||
# آخرین ارسالِ موفق (epoch) را در این فایل مینویسد (docs/operations.md «سرورِ اختصاصیِ جدید»). ست باشد، بر
|
||||
# OFFSITE_WAL_SLOT مقدم است. OFFSITE_WAL_SLOT را فقط ۲۴۹ دارد و با سوئیچ به ۲۱۶ همراهِ _slot_state حذف میشود.
|
||||
OFFSITE_SYNC_STATUS_FILE = os.environ.get('OFFSITE_SYNC_STATUS_FILE', '')
|
||||
# ارسال هر دقیقه است؛ ۱۰ دقیقه بیارسال یعنی چند اجرای پشتِ سرِ هم شکست خورده.
|
||||
OFFSITE_SYNC_MAX_AGE_SECONDS = 600
|
||||
|
||||
# چرخشِ فایلِ لاگ: 'size' (پیشفرض، Docker و محلی) = ۶ فایلِ ۱۰ مگی و قدیمیها پاک میشوند؛
|
||||
# 'external' (سرور) = برنامه هرگز پاک نمیکند، `ops/applogs/archive-app-log.sh` هر شب app.log را جابهجا و
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
پرسشِ واقعی از pg_replication_slots اینجا اجرا نمیشود (تستها روی SQLiteاند)؛ روی سرور با اسلاتِ
|
||||
واقعی دیده شد (docs/operations.md «بکاپ»).
|
||||
"""
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from django.test import SimpleTestCase, override_settings
|
||||
|
||||
from config import health
|
||||
@@ -27,13 +29,45 @@ class SlotStateTests(SimpleTestCase):
|
||||
self.assertEqual(health._slot_state((True, 'reserved', 100))['state'], 'ok')
|
||||
|
||||
|
||||
@override_settings(OFFSITE_SYNC_MAX_AGE_SECONDS=600)
|
||||
class PushedStateTests(SimpleTestCase):
|
||||
"""بکاپِ رمزشدهی ۲۱۶ (_pushed_state). پرسشِ pg_stat_archiver و خواندنِ فایل روی سرور دیده شد، نه اینجا."""
|
||||
|
||||
T0 = datetime(2026, 10, 17, 16, 0, tzinfo=timezone.utc)
|
||||
|
||||
def test_recent_sync_is_ok(self):
|
||||
r = health._pushed_state(self.T0, None, 1000, now=1060)
|
||||
self.assertEqual((r['state'], r['syncAgeSeconds']), ('ok', 60))
|
||||
self.assertEqual(r['lastArchivedAt'], self.T0.isoformat())
|
||||
|
||||
def test_old_sync_is_stale(self):
|
||||
self.assertEqual(health._pushed_state(self.T0, None, 1000, now=1601)['state'], 'sync_stale')
|
||||
self.assertEqual(health._pushed_state(self.T0, None, 1000, now=1600)['state'], 'ok')
|
||||
|
||||
def test_no_status_file_is_never_synced(self):
|
||||
self.assertEqual(health._pushed_state(self.T0, None, None, now=1000)['state'], 'never_synced')
|
||||
|
||||
def test_failure_after_last_success_wins(self):
|
||||
later = self.T0 + timedelta(minutes=1)
|
||||
self.assertEqual(health._pushed_state(self.T0, later, 1000, now=1001)['state'], 'archive_failing')
|
||||
self.assertEqual(health._pushed_state(None, later, 1000, now=1001)['state'], 'archive_failing')
|
||||
|
||||
def test_old_failure_then_success_is_ok(self):
|
||||
earlier = self.T0 - timedelta(minutes=1)
|
||||
self.assertEqual(health._pushed_state(self.T0, earlier, 1000, now=1001)['state'], 'ok')
|
||||
|
||||
def test_idle_database_with_no_archive_yet_is_ok(self):
|
||||
self.assertEqual(health._pushed_state(None, None, 1000, now=1001)['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'):
|
||||
for state in ('not_configured', 'slot_missing', 'disconnected', 'lagging', 'lost', 'error',
|
||||
'archive_failing', 'never_synced', 'sync_stale'):
|
||||
self.assertEqual(len(health._offsite_warnings({'state': state})), 1, state)
|
||||
|
||||
def test_sqlite_reports_not_postgres(self):
|
||||
|
||||
@@ -382,6 +382,104 @@ the servers»):
|
||||
- سنجیده (۲۰۲۶-۱۰-۱۳): اپ در Chrome، بیورود و با ورود، همهی تبها و آزمونِ یک جزیره، بی هیچ خطای CSP. **نسنجیده:** اپِ اندروید
|
||||
(WebView همان CSP را میگیرد) و صفحهی `pay.html` تا درگاه.
|
||||
|
||||
## سرورِ اختصاصیِ جدید (`78.38.35.216`): آماده، هنوز سوئیچ نشده
|
||||
|
||||
اپ به یک ماشینِ مجازیِ اختصاصی منتقل میشود. کاربران هنوز از ۲۴۹ سرو میشوند؛ ArvanCloud هنوز به ۲۴۹ اشاره میکند.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| ماشین | VMware، Ubuntu 24.04، ۸ هسته، ۱۲ گیگ رم، دیسکِ ۹۷ گیگ؛ hostname `mathapp`، IPِ داخلی `192.168.200.163` (۲۴۹ = `.164`)، IPِ خروجی `78.38.35.228` |
|
||||
| ورود | دو حساب با sudo: `zareian` (صاحبِ محصول) و `mathapp-admin` (مشترکِ سینیور و صاحبِ شرکت؛ رمزش در فایلِ رمز زیرِ `AdminUser`). `temp` حذف شد (۲۰۲۶-۱۰-۱۷؛ authorized_keysش در `/home/zareian/backups/`). ورودِ root بسته، `AllowUsers zareian mathapp-admin`، forwarding بسته (`10-tadrisino.conf`) |
|
||||
| فایلها | نسخهی اصلیِ همهی اسکریپتها و تنظیمهای این سرور در `ops/server216/` (بیکلید). تغییر: فایلِ ریپو، بعد کپی روی سرور |
|
||||
| ضدِ ربات | ۱) `tadrisino-blocklist.timer`: هر روز FireHOL level1 (Spamhaus، DShield، …) در ipsetِ `tadrisino-block`، همهی پورتها؛ شبکهی داخلی، `78.38.35.0/24` و رنجهای آروان هرگز در آن نیستند. ۲) `ufw limit 22`: بیش از ۶ اتصال در ۳۰ ثانیه رد. ۳) fail2ban پلهای: ۶ اشتباه در ۱۰ دقیقه → ۱۰ دقیقه، هر تکرار دو برابر تا یک هفته؛ `recidive`: ۵ بار در روز → یک هفته همهی پورتها؛ `192.168.200.0/24` هرگز. آزاد کردن: `sudo fail2ban-client set sshd unbanip <IP>` |
|
||||
| بکاپ | **رمزنگاری روی همین سرور، ۲۴۷ فقط فایلِ رمزشده میبیند** (۲۴۷ مشترک است و هر کس `administrator` را دارد root است). pgBackRest با `repo1-cipher-type=aes-256-cbc`: آرشیوِ پیوستهی WAL (`archive_timeout=60`، یعنی حداکثر یک دقیقه تغییرِ آرشیونشده)، `tadrisino-pgbackrest-full` یکشنبهها، `-diff` هر روز، دو نسخهی کامل نگه داشته میشود. CSVهای پرداخت هر ۶ ساعت با age رمز میشوند (`tadrisino-export-payment-events`). `tadrisino-offsite-sync` هر دقیقه `/var/lib/pgbackrest` و `payment-log` را به `~/zareian-pgbackrest-216/` روی ۲۴۷ میفرستد؛ کلیدِ postgres در `authorized_keys`ِ ۲۴۷ با `from=192.168.200.163,restrict,command="rrsync -wo …"` فقط میتواند در همان پوشه بنویسد (نه بخواند، نه شِل). `/api/health` → `offsiteBackup` از `pg_stat_archiver` و `OFFSITE_SYNC_STATUS_FILE` |
|
||||
| کلیدهای بازیابی | `/etc/tadrisino-backup/RECOVERY-KEYS` (root، 600): کلیدِ pgBackRest و کلیدِ age. **بیآنها بکاپِ ۲۴۷ خواندنی نیست**؛ یک نسخه بیرون از سرورها نزدِ صاحبِ محصول لازم است: `ssh -t zareian@78.38.35.216 sudo cat /etc/tadrisino-backup/RECOVERY-KEYS` در ترمینالِ خودش (نه در چت) |
|
||||
| آزمونِ بازیابی | `sudo -u postgres tadrisino-restore-check` (ماهی یک بار): بازیابی در پوشهی موقت و مقایسهی شمارشها با دیتابیسِ زنده. بازیابیِ واقعی از ۲۴۷ (اگر ۲۱۶ از دست رفت): پوشهی `repo/` را از ۲۴۷ کپی، `pgbackrest.conf` با کلید از RECOVERY-KEYS، `pgbackrest --stanza=zareian restore` |
|
||||
| اپ | کاربرِ بیsudoِ `mathapp`؛ `/home/mathapp/mathapp-v2` و `/home/mathapp/mathapp_frontend_new` |
|
||||
| سرویسها | `zareian-mathapp` (= `gunicorn-mathapp`، با `RuntimeDirectory`)، `zareian-redis` (6390)، `postgresql@16-zareian` (5440)، `nginx`، `zareian-applog-archive.timer` |
|
||||
| رمزها | رمزِ Postgres و Redis روی این سرور تازه ساخته شده (`.pg_password`، `.redis_password` کنارِ `.env`)، نه کپیِ ۲۴۹ |
|
||||
| فایروال | ufw فقط 22/80/443. فایروالِ شبکهی شرکت 443ِ ورودی را هنوز میبندد (80 باز است) |
|
||||
| بستهها | apt از `mirror.arvancloud.ir`، pip از `mirror-pypi.runflare.com` (`~mathapp/.config/pip/pip.conf`)، npm از `mirror-npm.runflare.com`؛ `files.pythonhosted.org` و `registry.npmjs.org` از این سرور بستهاند |
|
||||
|
||||
سنجیده: `/api/health` با همهی جزئیات ok، آزمونِ بازیابی از بکاپِ رمزشده با همان شمارشها، فایلهای ۲۴۷ رمزشده (`Salted__`،
|
||||
`age-encryption.org/v1`)، کلیدِ ۲۱۶ روی ۲۴۷ نه میخواند نه شِل میگیرد، کپیِ ۲۴۷ با `rsync --checksum` عینِ مخزنِ محلی، Qdrant از این سرور وصل، فرانت ۲۰۰، ریبوت و
|
||||
بالا آمدنِ خودکارِ همهی سرویسها، درخواستِ مستقیم به IP ۴۰۳ (nginx IPِ واقعی را میبیند)، بازیابیِ کامل از نسخهی رمزشدهی
|
||||
خودِ ۲۴۷ (دیتابیس و CSV؛ عقبماندگیِ طراحیشده حداکثر ~۲ دقیقه). سفرِ کاملِ کاربر از nginx+TLS روی خودِ سرور (۲۹ مورد): فرانت و
|
||||
هدرهای امنیتی، پیامکِ واقعیِ کاوهنگار و ورود، نقشه، آزمونِ جزیره و پایانش، پیِ زنده، سفارش و redirect به StartPayِ زرینپال (بیپرداخت)،
|
||||
خروج و باطل شدنِ توکن. کاوهنگار و GapGPT از IPِ خروجیِ این سرور پذیرفتهاند. نسنجیده: هیچ درخواستی از مسیرِ ArvanCloud
|
||||
(۴۴۳ بسته است)، پرداختِ کامل، خریدِ بازار.
|
||||
|
||||
### چه چیزی منتظرِ دیگران است
|
||||
|
||||
درخواست از سینیور فرستاده شده (۲۰۲۶-۱۰-۱۷): ۱) 443ِ ورودی روی فایروالِ شرکت؛ ۲) (انجام شد: `temp` حذف و `mathapp-admin` ساخته شد)؛ ۳) عوض کردنِ originِ
|
||||
`math.tadrisino2.ir` و `mathapi.tadrisino2.ir` در پنلِ ArvanCloud به `78.38.35.216` (ما به پنل دسترسی نداریم؛ همزمان با قدمِ ۴ پایین)؛
|
||||
۴) private کردنِ مخزن در Gitea؛ ۵) ۱۲ هسته و رمِ بیشتر (اختیاری؛ سرور ریبوت را تحمل میکند، تست شده). زرینپال و بازار به
|
||||
دامنه بستهاند نه IP، پس در پنلِ آنها چیزی عوض نمیشود.
|
||||
|
||||
### سوئیچ، قدمبهقدم (وقتی سینیور گفت آماده است)
|
||||
|
||||
1. **پیششرط:** از لپتاپ `curl -sk -o /dev/null -w '%{http_code}' https://78.38.35.216/` باید ۴۰۳ بدهد (نه ۰۰۰: یعنی 443 باز است و
|
||||
nginx فقط آروان را راه میدهد). کامیتها push شده باشند و `deploy.sh` روی هر دو سرور اجرا شده باشد (کدِ دو طرف یکی).
|
||||
2. **۲۴۹ را بخوابان:** `sudo systemctl stop zareian-mathapp` (از این لحظه کاربران خطا میبینند تا قدمِ ۵؛ کلِ کار ~۱۵ دقیقه).
|
||||
3. **دیتابیسِ نهایی:** `sudo -u postgres pg_dump -p 5440 -Fc zareian_mathapp` روی ۲۴۹، با pipe از لپتاپ به
|
||||
`/home/zareian/stage/` روی ۲۱۶ (مسیرِ داخلیِ ۲۴۹→۲۱۶ کلید ندارد).
|
||||
4. **روی ۲۱۶:** `systemctl stop zareian-mathapp`؛ `dropdb -p 5440 zareian_mathapp && createdb -p 5440 -O zareian_app zareian_mathapp` (با
|
||||
`sudo -u postgres`)؛ `pg_restore -p 5440 -d zareian_mathapp --no-owner --role=zareian_app --exit-on-error`؛ شمارشِ `accounts_user`،
|
||||
`content_question`، `payment_order`، `payment_paymentevent`، `content_useranswer` با همان پرسش روی ۲۴۹ یکی باشد؛ `systemctl start
|
||||
zareian-mathapp`؛ health با `X-Internal-Key` بیهشدار. دادههای تستِ ۲۰۲۶-۱۰-۱۷ روی ۲۱۶ با همین قدم پاک میشوند.
|
||||
5. **سینیور origin را در آروان عوض میکند.** بعد از بیرون: `https://mathapi.tadrisino2.ir/api/health` ok؛ در
|
||||
`/var/log/nginx/access.log`ِ ۲۱۶ IPهای آروان دیده شوند؛ ورود در Chrome (روشِ OTPِ ادمین، بخشِ «ورود بیپیامک») و یک آزمون؛ کاربرِ
|
||||
اندروید (WebView و `pay.html`) هم یک بار.
|
||||
6. **بکاپِ تازه:** `sudo systemctl start tadrisino-pgbackrest-full` و بعد `tadrisino-offsite-sync`؛ `sudo -u postgres tadrisino-restore-check`.
|
||||
7. **۲۴۹ را کنار بگذار (پاک نکن):** `sudo systemctl disable zareian-mathapp` روی ۲۴۹؛ اسلات را بردار تا WAL روی دیسکِ مشترک جمع نشود:
|
||||
`sudo -u postgres psql -p 5440 -c "select pg_drop_replication_slot('zareian_247')"`؛ روی ۲۴۷ `systemctl disable --now zareian-pg-tunnel
|
||||
zareian-pg-wal zareian-pg-basebackup.timer zareian-pg-export-events.timer zareian-applog-pull.timer`. دیتابیسِ ۲۴۹ یک هفته برای برگشت میماند.
|
||||
8. **برگشت (اگر قدمِ ۵ خراب شد):** origin در آروان دوباره ۲۴۹، و روی ۲۴۹ `systemctl start zareian-mathapp`. فقط تا وقتی درست است که ۲۱۶
|
||||
نوشتنِ واقعی (پرداخت، ثبتنام) نگرفته؛ بعد از آن اول dumpِ ۲۱۶ به ۲۴۹.
|
||||
9. **اسناد:** CLAUDE.md (سرورِ زنده = ۲۱۶؛ رویهی دیپلو با `zareian@78.38.35.216` و `sudo -u mathapp ./deploy.sh`)، این بخش، و
|
||||
`ops/nginx/` با مسیرهای `/home/mathapp` (۲۱۶ نسخهی sedشده را دارد).
|
||||
|
||||
### بعد از سوئیچ
|
||||
|
||||
- یک هفته بعد، با تأییدِ صاحبِ محصول: پاک کردنِ بکاپهای رمزنگارینشدهی ۲۴۹ روی ۲۴۷ (`~/zareian-pgbackup`، یونیتهای `zareian-pg-*`)
|
||||
و حذفِ `_slot_state`/`OFFSITE_WAL_SLOT` از `config/health.py` و `settings.py`.
|
||||
- لاگِ اپ: آرشیوِ شبانه روی ۲۱۶ هست (`zareian-applog-archive.timer`) ولی هنوز به ۲۴۷ نمیرود؛ راهِ پیشنهادی: افزودنِ
|
||||
`logs/archive` به `tadrisino-offsite-sync` (همان rrsync، پوشهی `app-logs/`).
|
||||
- ورود با کلیدِ SSH برای `zareian` (صاحبِ محصول خواسته فعلاً بماند؛ روی لپتاپ `ssh-keygen -t ed25519` و `ssh-copy-id`)، بعد
|
||||
`PasswordAuthentication no` با `Match Address 192.168.200.0/24` برای ورودِ با رمز از شبکهی داخلی.
|
||||
- توکنِ فقطخواندنیِ Gitea برای `deploy.sh` روی ۲۱۶ (بعد از private شدنِ مخزن، `git fetch` بیرمز دیگر کار نمیکند).
|
||||
- `CORS_ALLOWED_ORIGINS` در `.env`ِ سرور هنوز `localhost:5174` دارد؛ حذفش.
|
||||
- برنامهی صاحبِ محصول: مرورِ کاملِ یک فهرستِ امنیتیِ استاندارد (OWASP Top 10 / API Top 10 برای اپ، CIS Benchmark برای سرور، همراهِ
|
||||
`docs/security-before-production.md`)، بعد لودتست روی ۲۱۶ (`tools/loadtest/README.md`؛ با هستهی بیشتر: ۸ worker و `DB_POOL_MAX_SIZE` 6).
|
||||
|
||||
### Qdrant
|
||||
|
||||
**وضعِ فعلی Qdrant:** بردارهای RAG روی `https://qdrant.tadrisino2.ir` هستند (پشت ArvanCloud؛ سرورِ اصلی از بیرون پیدا
|
||||
نیست و در فهرستِ سرورهای ما نیست. `rag_catalog` آن را سرورِ تیمِ دیگر میداند). نسخهٔ 1.17.0، ۳۴ collection، حدود
|
||||
۱۰۸۸۰ نقطه، همه با بُعد ۱۵۳۶ و Cosine.
|
||||
|
||||
**هدف:** همهٔ کتابهای ریاضیای که در Qdrant هست (ریاضی، حسابان، هندسه، آمار و احتمال، گسسته؛ پایههای سوم تا دوازدهم) روی یک
|
||||
Qdrant اختصاصی ببریم و اپ را به آن وصل کنیم، بدون ingest دوباره، چون ingest هزینهٔ GapGPT دارد.
|
||||
|
||||
**کدام collectionها (۱۵ تا، حدود ۵۶۸۰ نقطه):**
|
||||
- ریاضی سوم، چهارم، پنجم، ششم، هفتم، هشتم، نهم، دهم.
|
||||
- حسابان یازدهم، دوازدهم.
|
||||
- هندسه دهم، یازدهم، دوازدهم.
|
||||
- آمار و احتمال یازدهم.
|
||||
- گسسته دوازدهم.
|
||||
|
||||
ریاضیِ سوم، ششم و هفتم را تیمِ دیگر ingest کرده، و صاحبِ محصول خواسته هر سه منتقل شوند. بقیهٔ collectionها (علوم، فارسی،
|
||||
مطالعات اجتماعی، هدیهٔ آسمان، پیامهای آسمان) در این برنامه نیستند.
|
||||
|
||||
**اصولِ انتقال:**
|
||||
- از هر collection یک snapshot بگیرید، فایلِ آن را منتقل و روی Qdrant جدید بارگذاری کنید (بدون فراخوانیِ OpenAI).
|
||||
- بعد از انتقال: تعدادِ نقاط، بُعد و Cosine یکی باشند؛ یک بردارِ ذخیرهشده را بهعنوانِ query بدهید و همان نقطه بالا بیاید.
|
||||
- `/api/rag/ingest` هنوز روی Qdrant فعلی مینویسد. در بازهٔ کپی ingest متوقف شود، یا بعد از سوئیچ نقاطِ تازه دوباره همگام شوند.
|
||||
- `QDRANT_URL` و `QDRANT_API_KEY` را فقط **بعد** از تأییدِ کپی عوض کنید؛ زودتر عوض شود، تیوتور جوابِ ناقص میدهد.
|
||||
|
||||
**باز:** محلِ سرورِ اصلیِ فعلیِ Qdrant (ادعای «روی سرورِ تیمِ دیگر» هنوز از روی سرور تأیید نشده). سرورِ جدید فعلاً به همان
|
||||
`qdrant.tadrisino2.ir` وصل است.
|
||||
|
||||
## لاگها
|
||||
|
||||
هر درخواستِ `/api/` بهصورتِ یک شیءِ JSON در هر خط لاگ میشود
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
#!/bin/bash
|
||||
# روی ۲۴۹، هر شب (zareian-applog-archive.timer). app.log را به logs/archive/ میبرد و فشرده میکند.
|
||||
# روی سرورِ اپ، هر شب (zareian-applog-archive.timer). app.log را به logs/archive/ میبرد و فشرده میکند.
|
||||
# برنامه با LOG_FILE_ROTATION=external از WatchedFileHandler استفاده میکند و پس از جابهجایی خودش app.log تازه
|
||||
# میسازد؛ خطی که در همان لحظه نوشته شود در فایلِ جابهجاشده میماند، گم نمیشود.
|
||||
# اینجا فقط ۳۰ روز میماند (دیسکِ ۲۴۹ پر است) و فقط فایلی پاک میشود که ۲۴۷ گرفتنش را تأیید کرده
|
||||
# (`.shipped`)؛ نسخهی همیشگی روی ۲۴۷ است (pull-app-logs.sh). فقط زیرِ ~/mathapp-v2/logs مینویسد.
|
||||
set -euo pipefail
|
||||
LOGS=/home/administrator/mathapp-v2/logs
|
||||
# پوشهی پروژه از جای خودِ اسکریپت (ops/applogs/ → دو پوشه بالاتر)، تا روی هر سروری با هر کاربری درست باشد.
|
||||
LOGS="$(cd "$(dirname "$0")/../.." && pwd)/logs"
|
||||
ARCH="$LOGS/archive"
|
||||
mkdir -p "$ARCH"
|
||||
chmod 750 "$ARCH"
|
||||
|
||||
13
ops/server216/10-tadrisino.conf
Normal file
13
ops/server216/10-tadrisino.conf
Normal file
@@ -0,0 +1,13 @@
|
||||
# تدریسینو: سختسازیِ SSH. فقط zareian (صاحبِ محصول) و mathapp-admin (حسابِ مشترکِ سینیور و صاحبِ شرکت) وارد میشوند.
|
||||
# ورود با رمز هنوز باز است (تا رفتن به کلید SSH)؛ fail2ban جلوی حدس زدن را میگیرد.
|
||||
PermitRootLogin no
|
||||
AllowUsers zareian mathapp-admin
|
||||
MaxAuthTries 4
|
||||
MaxSessions 10
|
||||
LoginGraceTime 2m
|
||||
X11Forwarding no
|
||||
AllowAgentForwarding no
|
||||
AllowTcpForwarding no
|
||||
PermitTunnel no
|
||||
ClientAliveInterval 300
|
||||
ClientAliveCountMax 2
|
||||
18
ops/server216/80-tadrisino-hardening.conf
Normal file
18
ops/server216/80-tadrisino-hardening.conf
Normal file
@@ -0,0 +1,18 @@
|
||||
# تدریسینو: تنظیماتِ شبکهی استاندارد (CIS). سرور مسیریاب نیست.
|
||||
net.ipv4.tcp_syncookies = 1
|
||||
net.ipv4.conf.all.rp_filter = 1
|
||||
net.ipv4.conf.default.rp_filter = 1
|
||||
net.ipv4.conf.all.accept_redirects = 0
|
||||
net.ipv4.conf.default.accept_redirects = 0
|
||||
net.ipv6.conf.all.accept_redirects = 0
|
||||
net.ipv6.conf.default.accept_redirects = 0
|
||||
net.ipv4.conf.all.send_redirects = 0
|
||||
net.ipv4.conf.default.send_redirects = 0
|
||||
net.ipv4.conf.all.accept_source_route = 0
|
||||
net.ipv6.conf.all.accept_source_route = 0
|
||||
net.ipv4.conf.all.log_martians = 1
|
||||
net.ipv4.icmp_echo_ignore_broadcasts = 1
|
||||
kernel.kptr_restrict = 2
|
||||
kernel.dmesg_restrict = 1
|
||||
fs.protected_hardlinks = 1
|
||||
fs.protected_symlinks = 1
|
||||
4
ops/server216/backup.conf
Normal file
4
ops/server216/backup.conf
Normal file
@@ -0,0 +1,4 @@
|
||||
# تدریسینو: آرشیوِ پیوستهی WAL با pgBackRest (رمزنگاریشده). archive_timeout یعنی حداکثر ۶۰ ثانیه تغییرِ آرشیونشده.
|
||||
archive_mode = on
|
||||
archive_command = 'pgbackrest --stanza=zareian archive-push %p'
|
||||
archive_timeout = 60
|
||||
20
ops/server216/gunicorn-mathapp.service
Normal file
20
ops/server216/gunicorn-mathapp.service
Normal file
@@ -0,0 +1,20 @@
|
||||
[Unit]
|
||||
Description=Gunicorn MathApp (tadrisino API)
|
||||
After=network.target postgresql@16-zareian.service zareian-redis.service
|
||||
Wants=postgresql@16-zareian.service zareian-redis.service
|
||||
|
||||
[Service]
|
||||
User=mathapp
|
||||
Group=www-data
|
||||
WorkingDirectory=/home/mathapp/mathapp-v2
|
||||
EnvironmentFile=/home/mathapp/mathapp-v2/.env
|
||||
# /run/gunicorn بعد از هر ریبوت ساخته میشود (روی ۲۴۹ دستی ساخته شده بود و بعد از ریبوت نبود).
|
||||
RuntimeDirectory=gunicorn
|
||||
RuntimeDirectoryMode=0750
|
||||
ExecStart=/home/mathapp/mathapp-v2/venv/bin/gunicorn --bind unix:/run/gunicorn/mathapp.sock config.wsgi:application
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
Alias=zareian-mathapp.service
|
||||
17
ops/server216/pgbackrest.conf
Normal file
17
ops/server216/pgbackrest.conf
Normal file
@@ -0,0 +1,17 @@
|
||||
# روی ۲۱۶: /etc/pgbackrest.conf (root:postgres 640). کلیدِ واقعی فقط روی سرور و در /etc/tadrisino-backup/RECOVERY-KEYS است.
|
||||
# تدریسینو: بکاپِ رمزنگاریشده. مخزنِ محلی /var/lib/pgbackrest؛ نسخهی بیرونی با tadrisino-offsite-sync به ۲۴۷ (فقط فایلِ رمزشده).
|
||||
[global]
|
||||
repo1-path=/var/lib/pgbackrest
|
||||
repo1-cipher-type=aes-256-cbc
|
||||
repo1-cipher-pass=<RECOVERY-KEYS، خطِ کلیدِ ۱>
|
||||
repo1-retention-full=2
|
||||
compress-type=zst
|
||||
process-max=2
|
||||
start-fast=y
|
||||
log-level-console=info
|
||||
log-level-file=detail
|
||||
|
||||
[zareian]
|
||||
pg1-path=/var/lib/postgresql/16/zareian
|
||||
pg1-port=5440
|
||||
pg1-socket-path=/var/run/postgresql
|
||||
44
ops/server216/tadrisino-blocklist
Executable file
44
ops/server216/tadrisino-blocklist
Executable file
@@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
# تدریسینو: IPهای شناختهشدهی مخرب را پیش از اولین تلاش روی همهی پورتها میبندد.
|
||||
# فهرست: FireHOL level1 (Spamhaus DROP/EDROP، DShield، Feodo، bogons)، روزی یک بار (tadrisino-blocklist.timer).
|
||||
# شبکههای داخلی، رنجِ IPِ شرکت و رنجهای ArvanCloud (از snippetِ nginx) هرگز بسته نمیشوند.
|
||||
# اگر دانلود نشد، آخرین فهرستِ سالم (/var/lib/tadrisino-blocklist/list) دوباره بار میشود.
|
||||
set -euo pipefail
|
||||
URL=https://iplists.firehol.org/files/firehol_level1.netset
|
||||
DIR=/var/lib/tadrisino-blocklist; SET=tadrisino-block
|
||||
mkdir -p "$DIR"
|
||||
tmp=$(mktemp); trap 'rm -f "$tmp"' EXIT
|
||||
if curl -fsS -m 90 "$URL" -o "$tmp" && [ "$(grep -c '^[0-9]' "$tmp")" -gt 1000 ]; then
|
||||
python3 - "$tmp" "$DIR/list" <<'PY'
|
||||
import ipaddress, re, sys
|
||||
src, dst = sys.argv[1], sys.argv[2]
|
||||
keep = ['127.0.0.0/8','10.0.0.0/8','172.16.0.0/12','192.168.0.0/16','100.64.0.0/10','169.254.0.0/16','78.38.35.0/24']
|
||||
try:
|
||||
keep += re.findall(r'^allow\s+([0-9./]+);', open('/etc/nginx/snippets/zareian-arvan-only.conf').read(), re.M)
|
||||
except OSError:
|
||||
pass
|
||||
keep = [ipaddress.ip_network(k) for k in keep if ':' not in k]
|
||||
out, dropped = [], 0
|
||||
for line in open(src):
|
||||
line = line.strip()
|
||||
if not line or line[0] == '#':
|
||||
continue
|
||||
n = ipaddress.ip_network(line, strict=False)
|
||||
if any(n.overlaps(k) for k in keep):
|
||||
dropped += 1
|
||||
continue
|
||||
out.append(str(n))
|
||||
open(dst, 'w').write('\n'.join(out) + '\n')
|
||||
print(f'{len(out)} networks, {dropped} skipped (internal/company/Arvan)')
|
||||
PY
|
||||
else
|
||||
echo "download failed; reloading last good list" >&2
|
||||
fi
|
||||
[ -s "$DIR/list" ] || { echo "no list available" >&2; exit 1; }
|
||||
ipset create "$SET" hash:net maxelem 262144 -exist
|
||||
ipset create "$SET-new" hash:net maxelem 262144 -exist
|
||||
ipset flush "$SET-new"
|
||||
sed "s/^/add $SET-new /" "$DIR/list" | ipset restore -exist
|
||||
ipset swap "$SET-new" "$SET"; ipset destroy "$SET-new"
|
||||
iptables -C INPUT -m set --match-set "$SET" src -j DROP 2>/dev/null || iptables -I INPUT 1 -m set --match-set "$SET" src -j DROP
|
||||
echo "loaded $(ipset list "$SET" -t | sed -n 's/^Number of entries: //p') entries into $SET"
|
||||
11
ops/server216/tadrisino-blocklist.service
Normal file
11
ops/server216/tadrisino-blocklist.service
Normal file
@@ -0,0 +1,11 @@
|
||||
[Unit]
|
||||
Description=Tadrisino: block known-malicious IPs (FireHOL level1)
|
||||
After=network-online.target ufw.service
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/sbin/tadrisino-blocklist
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
10
ops/server216/tadrisino-blocklist.timer
Normal file
10
ops/server216/tadrisino-blocklist.timer
Normal file
@@ -0,0 +1,10 @@
|
||||
[Unit]
|
||||
Description=Tadrisino: refresh malicious-IP blocklist daily
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 05:20:00
|
||||
RandomizedDelaySec=30m
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
19
ops/server216/tadrisino-export-payment-events
Executable file
19
ops/server216/tadrisino-export-payment-events
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
# تدریسینو: دفترِ خرید (payment_paymentevent)، سفارشها و تراکنشها به CSV، هر ۶ ساعت. هر فایل پیش از نوشتهشدن روی دیسک
|
||||
# با age برای کلیدِ /etc/tadrisino-backup/age-recipient.txt رمز میشود؛ متنِ خوانا هیچجا ذخیره نمیشود.
|
||||
# بازکردن: age -d -i age-identity.txt payment-events-….csv.age (کلید در RECOVERY-KEYS). ۱۴ فایلِ آخرِ هر نوع میماند.
|
||||
set -euo pipefail
|
||||
OUT=/var/lib/tadrisino-backup/payment-log
|
||||
REC=/etc/tadrisino-backup/age-recipient.txt
|
||||
stamp=$(date -u +%Y%m%dT%H%M%SZ)
|
||||
q() {
|
||||
psql -p 5440 -d zareian_mathapp -v ON_ERROR_STOP=1 -Atqc "\\copy ($1) to stdout csv header" | age -R "$REC" -o "$OUT/$2-$stamp.csv.age"
|
||||
}
|
||||
q "select id, created_at, order_id, user_id, provider, event, detail from payment_paymentevent order by id" payment-events
|
||||
q "select id, created_at, user_id, plan_id, plan_title, amount, status, provider, gateway_authority, subscription_id from payment_order order by id" orders
|
||||
q "select id, created_at, order_id, gateway, gateway_ref_id, status, paid_at from payment_transaction order by id" transactions
|
||||
chmod 640 "$OUT"/*-"$stamp".csv.age
|
||||
for kind in payment-events orders transactions; do
|
||||
ls -1t "$OUT/$kind"-*.csv.age | tail -n +15 | xargs -r rm -f --
|
||||
done
|
||||
echo "wrote $OUT/*-$stamp.csv.age"
|
||||
8
ops/server216/tadrisino-export-payment-events.service
Normal file
8
ops/server216/tadrisino-export-payment-events.service
Normal file
@@ -0,0 +1,8 @@
|
||||
[Unit]
|
||||
Description=Tadrisino: encrypted payment CSVs every 6 hours
|
||||
After=postgresql@16-zareian.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=postgres
|
||||
ExecStart=/usr/local/sbin/tadrisino-export-payment-events
|
||||
9
ops/server216/tadrisino-export-payment-events.timer
Normal file
9
ops/server216/tadrisino-export-payment-events.timer
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Tadrisino: encrypted payment CSVs every 6 hours
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 01,07,13,19:15:00
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
11
ops/server216/tadrisino-offsite-sync
Executable file
11
ops/server216/tadrisino-offsite-sync
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
# تدریسینو: نسخهی بیرونیِ بکاپ. فقط فایلهای رمزشده (مخزنِ pgBackRest با AES-256 و CSVهای age) به ۲۴۷ میروند.
|
||||
# کلیدِ postgres روی ۲۴۷ با rrsync -wo محدود است: فقط نوشتن در ~/zareian-pgbackrest-216، نه خواندن، نه شِل.
|
||||
# پس از موفقیتِ هر دو، زمانش در offsite-synced-at ثبت میشود و /api/health از روی آن عقبماندگی را میسنجد.
|
||||
set -euo pipefail
|
||||
R=administrator@192.168.200.167
|
||||
SSH="ssh -i /var/lib/postgresql/.ssh/id_offsite -o BatchMode=yes -o ConnectTimeout=20"
|
||||
rsync -a --delete -e "$SSH" /var/lib/pgbackrest/ "$R:repo/"
|
||||
rsync -a --delete -e "$SSH" /var/lib/tadrisino-backup/payment-log/ "$R:payment-log/"
|
||||
date -u +%s > /var/lib/tadrisino-backup/offsite-synced-at.tmp
|
||||
mv /var/lib/tadrisino-backup/offsite-synced-at.tmp /var/lib/tadrisino-backup/offsite-synced-at
|
||||
8
ops/server216/tadrisino-offsite-sync.service
Normal file
8
ops/server216/tadrisino-offsite-sync.service
Normal file
@@ -0,0 +1,8 @@
|
||||
[Unit]
|
||||
Description=Tadrisino: copy encrypted backups to 247
|
||||
After=postgresql@16-zareian.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=postgres
|
||||
ExecStart=/usr/local/sbin/tadrisino-offsite-sync
|
||||
9
ops/server216/tadrisino-offsite-sync.timer
Normal file
9
ops/server216/tadrisino-offsite-sync.timer
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Tadrisino: copy encrypted backups to 247
|
||||
|
||||
[Timer]
|
||||
OnCalendar=minutely
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
8
ops/server216/tadrisino-pgbackrest-diff.service
Normal file
8
ops/server216/tadrisino-pgbackrest-diff.service
Normal file
@@ -0,0 +1,8 @@
|
||||
[Unit]
|
||||
Description=Tadrisino: daily differential encrypted backup
|
||||
After=postgresql@16-zareian.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=postgres
|
||||
ExecStart=/usr/bin/pgbackrest --stanza=zareian --type=diff backup
|
||||
9
ops/server216/tadrisino-pgbackrest-diff.timer
Normal file
9
ops/server216/tadrisino-pgbackrest-diff.timer
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Tadrisino: daily differential encrypted backup
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 03:40:00
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
8
ops/server216/tadrisino-pgbackrest-full.service
Normal file
8
ops/server216/tadrisino-pgbackrest-full.service
Normal file
@@ -0,0 +1,8 @@
|
||||
[Unit]
|
||||
Description=Tadrisino: weekly full encrypted backup
|
||||
After=postgresql@16-zareian.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=postgres
|
||||
ExecStart=/usr/bin/pgbackrest --stanza=zareian --type=full backup
|
||||
9
ops/server216/tadrisino-pgbackrest-full.timer
Normal file
9
ops/server216/tadrisino-pgbackrest-full.timer
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Tadrisino: weekly full encrypted backup
|
||||
|
||||
[Timer]
|
||||
OnCalendar=Sun *-*-* 03:00:00
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
26
ops/server216/tadrisino-restore-check
Executable file
26
ops/server216/tadrisino-restore-check
Executable file
@@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
# تدریسینو: آزمونِ بازیابی از بکاپِ رمزشده (ماهی یک بار، دستی: sudo -u postgres tadrisino-restore-check).
|
||||
# آخرین بکاپ + همهی WALِ آرشیوشده را در پوشهی موقت بازیابی میکند، Postgresِ موقت را روی سوکتِ همان پوشه (بیپورتِ شبکه،
|
||||
# archive_mode=off تا چیزی به مخزن ننویسد) بالا میآورد، شمارشها را کنارِ دیتابیسِ اصلی چاپ میکند و همه را پاک میکند.
|
||||
set -euo pipefail
|
||||
BIN=/usr/lib/postgresql/16/bin
|
||||
work=$(mktemp -d /var/lib/postgresql/restore-check.XXXXXX)
|
||||
trap '"$BIN/pg_ctl" -D "$work/data" -m immediate stop >/dev/null 2>&1 || true; rm -rf "$work"' EXIT
|
||||
pgbackrest --stanza=zareian --pg1-path="$work/data" --type=default --archive-mode=off restore --log-level-console=warn
|
||||
cat > "$work/data/postgresql.conf" <<CONF
|
||||
listen_addresses = ''
|
||||
port = 5499
|
||||
unix_socket_directories = '$work'
|
||||
archive_mode = off
|
||||
shared_buffers = 128MB
|
||||
CONF
|
||||
echo 'local all all peer' > "$work/data/pg_hba.conf"
|
||||
"$BIN/pg_ctl" -D "$work/data" -l "$work/log" -w -t 600 start >/dev/null
|
||||
for _ in $(seq 120); do
|
||||
[ "$(psql -h "$work" -p 5499 -d zareian_mathapp -Atc 'select pg_is_in_recovery()')" = f ] && break; sleep 1
|
||||
done
|
||||
Q="select 'users', count(*) from accounts_user union all select 'questions', count(*) from content_question
|
||||
union all select 'orders', count(*) from payment_order union all select 'payment_events', count(*) from payment_paymentevent
|
||||
union all select 'login_events', count(*) from accounts_loginevent"
|
||||
echo "restored | live"
|
||||
paste -d'|' <(psql -h "$work" -p 5499 -d zareian_mathapp -Atc "$Q") <(psql -p 5440 -d zareian_mathapp -Atc "$Q" | cut -d'|' -f2)
|
||||
25
ops/server216/tadrisino.local
Normal file
25
ops/server216/tadrisino.local
Normal file
@@ -0,0 +1,25 @@
|
||||
# تدریسینو: SSH در برابرِ رباتهای حدسِ رمز، پلهای تا آدمِ واقعی (اشتباهِ تایپی، کیبوردِ فارسی) گیر نکند.
|
||||
# ۶ اشتباه در ۱۰ دقیقه → ۱۰ دقیقه بسته؛ هر بارِ تکرار دو برابر (۲۰، ۴۰، ...، حداکثر یک هفته).
|
||||
# شبکهی داخلیِ شرکت (ادمینها، ۲۴۷، ۲۴۹) هرگز بسته نمیشود.
|
||||
[DEFAULT]
|
||||
ignoreip = 127.0.0.1/8 ::1 192.168.200.0/24
|
||||
bantime.increment = true
|
||||
bantime.factor = 2
|
||||
bantime.maxtime = 1w
|
||||
|
||||
[sshd]
|
||||
enabled = true
|
||||
backend = systemd
|
||||
mode = aggressive
|
||||
maxretry = 6
|
||||
findtime = 10m
|
||||
bantime = 10m
|
||||
|
||||
# فقط رباتِ سمج: پنج بار بستهشدن در یک روز → یک هفته روی همهی پورتها.
|
||||
[recidive]
|
||||
enabled = true
|
||||
logpath = /var/log/fail2ban.log
|
||||
banaction = iptables-allports
|
||||
findtime = 1d
|
||||
maxretry = 5
|
||||
bantime = 1w
|
||||
13
ops/server216/zareian-redis.service
Normal file
13
ops/server216/zareian-redis.service
Normal file
@@ -0,0 +1,13 @@
|
||||
[Unit]
|
||||
Description=Redis for zareian-mathapp only
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=redis
|
||||
Group=redis
|
||||
ExecStart=/usr/bin/redis-server /etc/zareian-redis.conf
|
||||
Restart=on-failure
|
||||
NoNewPrivileges=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
5
ops/server216/zareian.conf
Normal file
5
ops/server216/zareian.conf
Normal file
@@ -0,0 +1,5 @@
|
||||
# تدریسینو، سرور اختصاصی: فقط localhost. max_connections با workers × DB_POOL_MAX_SIZE + 6 در /api/health سنجیده میشود.
|
||||
listen_addresses = '127.0.0.1'
|
||||
max_connections = 60
|
||||
shared_buffers = 1GB
|
||||
effective_cache_size = 4GB
|
||||
Reference in New Issue
Block a user