Files
Bridge-and-Join-s/deploy/linux/sbin/bj-backup
T
zuevav 4adc1398f6 База bj на своём PostgreSQL; «Не заполнено: …» для пустых полей; запись канала ИШ — в фоне, с ходом на странице
- шаг «База bj»: перенос на сервер PostgreSQL организации (проверка подключения, версии, прав, пустой базы;
  pg_dump → psql; сверка числа строк по таблицам; переключение и перезапуск); прежняя база — запасная копия;
  резервные копии: дамп внешней базы по её подключению, восстановление её не трогает
- все формы: пустые видимые обязательные поля называются по имени и подсвечиваются; DEPO — и на сервере
- «Обновить/Создать канал»: в фоне bj, ход по этапам с таймером (и после перезагрузки страницы), переходы
  заблокированы до конца, в конце проверка, что шлюз загрузил канал; состояние ИШ во время записи — без
  очереди к root-обработчику (страница больше не висит на «Проверяю…»)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
2026-10-07 00:00:35 +03:00

114 lines
7.0 KiB
Bash
Executable File

#!/bin/bash
# bj-backup — резервная копия сервера bj на сетевое хранилище (SMB): базы PostgreSQL bj и igate,
# данные bj (/var/lib/bj/.bj, /etc/bj) и шлюза НРД (/var/lib/igate без журналов).
# Куда, как часто и сколько хранить — /etc/bj/backup.conf (пишет bj-backup-admin из настроек bj),
# логин хранилища — /etc/bj/backup-smb.cred (только root). Итог — /var/lib/bj/backup-status.json.
set -uo pipefail
CONF=/etc/bj/backup.conf
CRED=/etc/bj/backup-smb.cred
STATUS=/var/lib/bj/backup-status.json
[ -r "$CONF" ] && . "$CONF"
SHARE=${SHARE:-}
DIR=${DIR:-}
# smbloc — адрес хранилища, как его вводят (\\nas\backup\bj как в проводнике, //nas/backup, smb://…, сервер
# в «Хранилище» + общая папка в «Папке») → две строки: общая папка для smbclient (//сервер/папка) и путь внутри неё
smbloc(){ python3 - "$1" "$2" <<'PY'
import re,sys
def parts(s):
s=s.strip().replace('\\','/')
m=re.match(r'(?i)smb://',s)
if m: s=s[m.end():]
return [p.strip() for p in s.split('/') if p.strip()]
sp,dp=parts(sys.argv[1]),parts(sys.argv[2])
if not sp: print(''); print('/'.join(dp)); sys.exit()
if len(sp)==1:
if not dp: print('//'+sp[0]); print(''); sys.exit()
sp,dp=[sp[0],dp[0]],dp[1:]
print('//'+sp[0]+'/'+sp[1]); print('/'.join(sp[2:]+dp))
PY
}
mapfile -t _loc < <(smbloc "$SHARE" "$DIR"); SHARE=${_loc[0]:-}; DIR=${_loc[1]:-bj-backup}
KEEP_DAYS=${KEEP_DAYS:-14}
TS=$(date +%Y-%m-%d_%H%M)
W=$(mktemp -d /var/tmp/bj-backup.XXXXXX)
trap 'rm -rf "$W"' EXIT
cd /
status() { python3 - "$STATUS" "$1" "$DIR/$TS" "$2" <<'PY'
import json,sys,datetime
p,ok,d,msg=sys.argv[1:]
json.dump({"at":datetime.datetime.now().astimezone().isoformat(timespec="seconds"),"ok":ok=="true","dir":d,"msg":msg},open(p,"w"),ensure_ascii=False)
PY
chown bj:bj "$STATUS" 2>/dev/null || true; }
fail() { status false "$1"; echo "ОШИБКА: $1" >&2; exit 1; }
smb() { smbclient "$SHARE" -A "$CRED" -m SMB3 -c "$1"; }
# tar: «файл изменился во время чтения» (код 1) для работающих служб — не ошибка
tarok() { tar "$@"; local rc=$?; [ $rc -le 1 ]; }
[ -n "$SHARE" ] || fail "не задано хранилище (Настройка → Резервное копирование)"
[ -r "$CRED" ] || fail "нет логина хранилища (Настройка → Резервное копирование)"
# база bj: на этом сервере — от имени postgres; на сервере PostgreSQL организации (перенесена в мастере) — по её
# подключению из настроек bj. Сервер организации новее нашего pg_dump — копия без базы, с пометкой: базу на сервере
# организации резервирует её администратор баз.
DBNOTE=""
mapfile -t BJDB < <(python3 - <<'PY'
import json,urllib.parse
try:
d=json.load(open("/var/lib/bj/.bj/setup.json")).get("postgres",{}).get("dsn","")
u=urllib.parse.urlparse(d); h=(u.hostname or "").lower()
if d and h not in ("","localhost","127.0.0.1","::1") and not h.startswith("/"):
host="["+u.hostname+"]" if ":" in u.hostname else u.hostname
netloc=(urllib.parse.quote(u.username or "")+"@" if u.username else "")+host+(":"+str(u.port) if u.port else "")
print("ext"); print(urllib.parse.urlunparse((u.scheme,netloc,u.path,"",u.query,""))); print(urllib.parse.unquote(u.password or "")); print(u.hostname)
except Exception: pass
PY
)
if [ "${BJDB[0]:-}" = ext ]; then
if ! PGPASSWORD="${BJDB[2]:-}" pg_dump -Fc --no-owner --no-privileges --dbname="${BJDB[1]}" > "$W/bj.pgdump" 2>"$W/pgdump.err"; then
rm -f "$W/bj.pgdump"
DBNOTE="база bj на сервере организации ${BJDB[3]} в копию не вошла ($(grep -v '^\s*$' "$W/pgdump.err" | tail -1 | head -c 160)) — её резервирует администратор баз"
else
DBNOTE="база bj — с сервера организации ${BJDB[3]}"
fi
rm -f "$W/pgdump.err"
else
runuser -u postgres -- pg_dump -Fc bj > "$W/bj.pgdump" || fail "дамп базы bj"
fi
if runuser -u postgres -- psql -Atc "select 1 from pg_database where datname='igate'" 2>/dev/null | grep -q 1; then
runuser -u postgres -- pg_dump -Fc igate > "$W/igate.pgdump" || fail "дамп базы igate"
fi
# Не копируем: пароль хранилища; то, что принадлежит этой машине (отпечаток железа, список установленных
# компонентов — на новом сервере они свои); ключевые носители (флешка ключа монтируется в
# /var/lib/igate/keymedia): ключи СКЗИ не должны уходить на сетевое хранилище — при восстановлении носитель
# подключают заново. --one-file-system — не заходить ни в какие смонтированные носители.
tarok -C / -czf "$W/bj-data.tar.gz" --one-file-system --exclude=etc/bj/backup-smb.cred --exclude=etc/bj/hw-id \
--exclude=var/lib/bj/.bj/components.json var/lib/bj/.bj etc/bj || fail "архив данных bj"
if [ -d /var/lib/igate ]; then
tarok -C / -czf "$W/igate-data.tar.gz" --one-file-system --warning=no-file-changed --exclude='var/lib/igate/.cache' \
--exclude='var/lib/igate/keymedia/*' --exclude='var/lib/igate/NSD Integration Gateway/logs*' \
--exclude='var/lib/igate/logs-cli' var/lib/igate || fail "архив шлюза НРД"
fi
cat > "$W/backup-info.txt" <<INFO
Bridge&Join — резервная копия сервера $(hostname) от $(date '+%d.%m.%Y %H:%M:%S')
Версия bj: $(/opt/bj/bj-server -version 2>/dev/null | head -1)
Восстановление: мастер настройки → «Восстановить из резервной копии» или bj-backup-admin restore.
INFO
(cd "$W" && sha256sum * > SHA256SUMS)
SIZE=$(du -sh "$W" | cut -f1)
top=${DIR%%/*}
smb "mkdir \"$top\"; mkdir \"$DIR\"" >/dev/null 2>&1 || true
smb "mkdir \"$DIR/$TS\"; cd \"$DIR/$TS\"; lcd $W; prompt; mput *" >/dev/null || fail "запись на $SHARE/$DIR"
for f in $(cd "$W" && ls); do
smb "cd \"$DIR/$TS\"; ls \"$f\"" >/dev/null 2>&1 || fail "на хранилище нет $f"
done
CUT=$(date -d "-$KEEP_DAYS days" +%Y-%m-%d_%H%M)
smb "cd \"$DIR\"; ls" 2>/dev/null | awk '{print $1}' | grep -E '^[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{4}$' | while read -r d; do
if [[ "$d" < "$CUT" ]]; then
smb "cd \"$DIR/$d\"; prompt; mask *; mdel *; cd ..; rmdir \"$d\"" >/dev/null 2>&1 && echo "удалена старая копия $d"
fi
done
status true "размер $SIZE${DBNOTE:+; $DBNOTE}"
echo "копия $DIR/$TS готова ($SIZE)${DBNOTE:+; $DBNOTE}"