#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ ПРОВЕРКА, ЧТО БЭКАП РАЗВОРАЧИВАЕТСЯ. Дамп, который никто не разворачивал, — это надежда, а не резервная копия. Проверяем три вещи, по возрастанию строгости: 1. контрольная сумма сходится с той, что лежит рядом в бакете; 2. архив читается `pg_restore --list` и содержит ожидаемое число таблиц; 3. ключевые торговые таблицы присутствуют поимённо. Ничего не восстанавливаем в боевую базу — только читаем оглавление архива. Полная проверка (развернуть в отдельную базу и сверить число строк) — отдельно и в тихое время. python3 backup_verify.py """ import json, os, re, subprocess, sys, tempfile BUCKET = "gs://arena-pg-backups-decoded-reducer" PG_CONTAINER = "postgres" EXPECT_MIN_TABLES = 300 # 213 trades_* + equity_* + служебные KEY_TABLES = ["rejected_signals", "grid_lab_trades", "arena_l2_binance", "arena_equity_snapshots"] def sh(c, t=900): r = subprocess.run(c, shell=True, capture_output=True, text=True, timeout=t) return (r.stdout or "") + (r.stderr or "") print("=== 1. ПОСЛЕДНИЙ ДАМП В БАКЕТЕ ===") ls = sh(f"gcloud storage ls {BUCKET}/ 2>/dev/null") dumps = sorted(x.strip() for x in ls.splitlines() if x.strip().endswith(".dump")) if not dumps: sys.exit("⛔ дампов в бакете не найдено — проверить права и имя бакета") last = dumps[-1] name = last.rsplit("/", 1)[-1] print(f" {name}") print(f" всего дампов в бакете: {len(dumps)}") tmp = "/tmp/bkp_verify" os.makedirs(tmp, exist_ok=True) local = os.path.join(tmp, name) print("\n=== 2. СКАЧИВАНИЕ ===") if os.path.exists(local) and os.path.getsize(local) > 1_000_000: print(f" уже скачан: {os.path.getsize(local)/1048576:.0f} МБ") else: sh(f"gcloud storage cp '{last}' '{local}'") if not os.path.exists(local): sys.exit("⛔ скачать не удалось") print(f" скачано: {os.path.getsize(local)/1048576:.0f} МБ") print("\n=== 3. КОНТРОЛЬНАЯ СУММА ===") sha_remote = sh(f"gcloud storage cat '{last}.sha256' 2>/dev/null").strip() if not sha_remote: print(" ⚠️ файла .sha256 рядом нет — сверить не с чем") else: want = sha_remote.split()[0] got = sh(f"sha256sum '{local}'").split()[0] ok = want == got print(f" в бакете: {want[:32]}…") print(f" посчитано: {got[:32]}…") print(f" {'✅ СХОДИТСЯ' if ok else '❌ НЕ СХОДИТСЯ — дамп повреждён при передаче или записи'}") print("\n=== 4. ЧИТАЕТСЯ ЛИ АРХИВ ===") sh(f"docker cp '{local}' {PG_CONTAINER}:/tmp/verify.dump") toc = sh(f"docker exec {PG_CONTAINER} pg_restore --list /tmp/verify.dump 2>&1") if "error" in toc.lower() and "TABLE DATA" not in toc: print(" ❌ архив не читается:") print(" " + toc.strip().splitlines()[0][:100]) sys.exit(1) n_data = len(re.findall(r"TABLE DATA", toc)) n_tables = len(re.findall(r"\bTABLE\b(?! DATA)", toc)) n_idx = len(re.findall(r"\bINDEX\b", toc)) print(f" ✅ архив читается") print(f" таблиц с данными : {n_data}") print(f" определений таблиц: {n_tables}") print(f" индексов : {n_idx}") print(f" {'✅' if n_data >= EXPECT_MIN_TABLES else '⚠️ '} " f"ожидали минимум {EXPECT_MIN_TABLES} таблиц с данными") print("\n=== 5. КЛЮЧЕВЫЕ ТАБЛИЦЫ НА МЕСТЕ ===") for t in KEY_TABLES: present = re.search(rf"TABLE DATA public {re.escape(t)}\b", toc) is not None print(f" {'✅' if present else '❌'} {t}") print("\n=== ВЕРДИКТ ===") good = (n_data >= EXPECT_MIN_TABLES and all(re.search(rf"TABLE DATA public {re.escape(t)}\b", toc) for t in KEY_TABLES)) print(" " + ("БЭКАП ПРИГОДЕН: сумма сходится, архив читается, таблицы на месте" if good else "ЕСТЬ ВОПРОСЫ — см. выше")) print("\n Полная проверка (раз в месяц): развернуть в отдельную базу") print(" sovereign_restoretest и сверить число строк с боевой.") sh(f"docker exec {PG_CONTAINER} rm -f /tmp/verify.dump")