Files
Bridge-and-Join-s/deploy/linux/sbin/bj-backup-admin
T
zuevav c62225dd7d ИШ ждёт флешку с ключом — bj это видит и перезапускает шлюз сам; итог установки ключа; список копий; скрытые блоки
- ИШ без флешки ждёт её в окне Валидаты («Вставьте ключевой носитель»): статус «ждёт флешку», без кнопки
  переустановки; сторож не тратит перезапуски, а когда флешка появилась — перезапускает ИШ сам; письмо
- шаг ключа: итог — карточка «Ключ подключён», «Заменить ключ» вместо «Установить ещё один», этапы свёрнуты
- копии: «Копии на хранилище» ищет там же, куда ложится копия (подпапка по умолчанию bj-backup, не из старых
  настроек); поля показываются разобранными
- [hidden] прячет всегда (пустая полоска под версией ИШ); «Шлюз НРД на другом сервере?» — что это за адрес

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

240 lines
18 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# bj-backup-admin — управление резервным копированием bj (вызывает root-демон bj-keymedia по запросу из
# настроек bj). Параметры — JSON в BJ_EXTRA: share, dir, user, password, every_hours, keep_days, enabled, name.
# config — сохранить хранилище, расписание и хранение; включить/выключить таймер
# test — проверить доступ на запись к хранилищу
# run — сделать копию сейчас (в фоне)
# list — список копий на хранилище
# verify — проверить копию name: скачать и сверить контрольные суммы (ничего не восстанавливает)
# restore — восстановить копию name (в фоне: останавливает bj и шлюз НРД)
set -uo pipefail
CONF=/etc/bj/backup.conf
CRED=/etc/bj/backup-smb.cred
RSTATUS=/var/lib/bj/restore-status.json
cmd=${1:-}
j() { python3 -c "import json,os,sys;print(json.loads(os.environ.get('BJ_EXTRA') or '{}').get(sys.argv[1],''))" "$1"; }
q() { python3 -c "import json,sys;print(json.dumps(sys.argv[1],ensure_ascii=False))" "$1"; }
ok() { echo "{\"ok\":true,\"output\":$(q "$1")}"; }
err() { echo "{\"ok\":false,\"error\":$(q "$1")}"; exit 0; }
[ -r "$CONF" ] && . "$CONF"
# хранилище из запроса (восстановление на новом сервере, проверка до сохранения) или из настроек
SHARE_IN=$(j share); DIR_IN=$(j dir); USER_IN=$(j user); PASS_IN=$(j password)
# хранилище из запроса — и подпапка из запроса (не из прежних настроек: иначе список копий искал не там)
if [ -n "$SHARE_IN" ]; then SHARE=$SHARE_IN; DIR=$DIR_IN; else SHARE=${SHARE:-}; DIR=${DIR:-}; fi
# 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}
TMPCRED=""
credfile() {
if [ -n "$USER_IN" ] && [ -n "$PASS_IN" ]; then
TMPCRED=$(mktemp /run/bj-backup-cred.XXXXXX); chmod 600 "$TMPCRED"
printf 'username=%s\npassword=%s\n' "$USER_IN" "$PASS_IN" > "$TMPCRED"; echo "$TMPCRED"
else echo "$CRED"; fi
}
trap '[ -n "$TMPCRED" ] && rm -f "$TMPCRED"' EXIT
# smbclient ставит установщик; на серверах, установленных раньше, доставляем при первом обращении
case "$cmd" in config|test|list|verify|restore)
command -v smbclient >/dev/null 2>&1 || DEBIAN_FRONTEND=noninteractive apt-get install -y -qq smbclient >/dev/null 2>&1
command -v smbclient >/dev/null 2>&1 || err "на сервере нет smbclient и его не удалось установить (apt-get install smbclient)" ;;
esac
smbx() { smbclient "$SHARE" -A "$1" -m SMB3 -c "$2" 2>&1; }
# расписание таймера: каждые N часов (24 — раз в сутки в 02:00)
schedule() {
install -d -m 755 /etc/systemd/system/bj-backup.timer.d
if [ "$1" = 24 ]; then cal='*-*-* 02:00:00'; else cal="*-*-* 00/$1:00:00"; fi
printf '[Timer]\nOnCalendar=\nOnCalendar=%s\n' "$cal" > /etc/systemd/system/bj-backup.timer.d/schedule.conf
systemctl daemon-reload
}
case "$cmd" in
config)
every=$(j every_hours); keep=$(j keep_days); en=$(j enabled)
case "$every" in 1|2|3|4|6|8|12|24) ;; *) every=6 ;; esac
[[ "$keep" =~ ^[0-9]+$ ]] && [ "$keep" -ge 1 ] || keep=14
[ -n "$SHARE" ] || err "не указано хранилище"
install -d -m 755 /etc/bj
umask 077
case "$en" in True|true) enabled=1 ;; *) enabled=0 ;; esac
printf 'SHARE=%q\nDIR=%q\nKEEP_DAYS=%s\nEVERY_HOURS=%s\nENABLED=%s\n' "$SHARE" "$DIR" "$keep" "$every" "$enabled" > "$CONF"
if [ -n "$USER_IN" ] && [ -n "$PASS_IN" ]; then
printf 'username=%s\npassword=%s\n' "$USER_IN" "$PASS_IN" > "$CRED"; chmod 600 "$CRED"
elif [ -n "$USER_IN" ] && [ -r "$CRED" ]; then # сменили только логин — пароль прежний
sed -i "s/^username=.*/username=$USER_IN/" "$CRED"
fi
schedule "$every"
if [ "$enabled" = 1 ]; then
systemctl enable --now bj-backup.timer >/dev/null 2>&1 || err "не удалось включить таймер bj-backup.timer"
ok "сохранено: $SHARE/$DIR, каждые $every ч, хранение $keep дн., расписание включено"
else
systemctl disable --now bj-backup.timer >/dev/null 2>&1
ok "сохранено, расписание выключено"
fi ;;
test)
[ -n "$SHARE" ] || err "не указано хранилище"
c=$(credfile); [ -r "$c" ] || err "не указан логин и пароль хранилища"
f=/tmp/bj-backup-test.$$; echo test > "$f"; top=${DIR%%/*}
smbx "$c" "mkdir \"$top\"; mkdir \"$DIR\"" >/dev/null
out=$(smbx "$c" "cd \"$DIR\"; put $f bj-write-test.txt; del bj-write-test.txt"); rc=$?; rm -f "$f"
if [ $rc -ne 0 ] || printf '%s' "$out" | grep -q "NT_STATUS"; then
why=$(printf '%s' "$out" | grep -o 'NT_STATUS[A-Z_]*' | head -1)
case "$why" in
NT_STATUS_BAD_NETWORK_NAME) why="на сервере нет общей папки «${SHARE##*/}» ($why)" ;;
NT_STATUS_LOGON_FAILURE) why="неверный логин или пароль ($why)" ;;
NT_STATUS_ACCESS_DENIED) why="у логина нет прав на запись ($why)" ;;
NT_STATUS_HOST_UNREACHABLE|NT_STATUS_IO_TIMEOUT|NT_STATUS_CONNECTION_REFUSED) why="сервер хранилища недоступен ($why)" ;;
"") why=$(printf '%s' "$out" | grep -v '^\s*$' | tail -1) ;;
esac
err "нет доступа на запись в $SHARE/$DIR: ${why:-smbclient завершился с кодом $rc}"
fi
ok "доступ на запись к $SHARE/$DIR есть" ;;
run)
systemctl start --no-block bj-backup.service || err "не удалось запустить bj-backup.service"
ok "копирование запущено" ;;
list)
[ -n "$SHARE" ] || err "не указано хранилище"
c=$(credfile); [ -r "$c" ] || err "не указан логин и пароль хранилища"
out=$(smbx "$c" "cd \"$DIR\"; ls")
printf '%s' "$out" | grep -q "NT_STATUS" && err "хранилище: $(printf '%s' "$out" | grep -o 'NT_STATUS[A-Z_]*' | head -1)"
names=$(printf '%s\n' "$out" | awk '{print $1}' | grep -E '^[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{4}$' | sort -r | head -200)
echo "{\"ok\":true,\"list\":$(python3 -c 'import json,sys;print(json.dumps([l for l in sys.stdin.read().split() if l]))' <<< "$names")}" ;;
verify)
name=$(j name)
[[ "$name" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{4}$ ]] || err "не выбрана копия"
[ -n "$SHARE" ] || err "не указано хранилище"
c=$(credfile); [ -r "$c" ] || err "не указан логин и пароль хранилища"
W=$(mktemp -d /var/tmp/bj-verify.XXXXXX); trap 'rm -rf "$W"; [ -n "$TMPCRED" ] && rm -f "$TMPCRED"' EXIT
out=$(smbclient "$SHARE" -A "$c" -m SMB3 -c "cd \"$DIR/$name\"; lcd $W; prompt; mget *" 2>&1)
printf '%s' "$out" | grep -q "NT_STATUS" && err "не удалось скачать копию $name: $(printf '%s' "$out" | grep -o 'NT_STATUS[A-Z_]*' | head -1)"
[ -s "$W/SHA256SUMS" ] || err "в копии $name нет SHA256SUMS — копия неполная"
(cd "$W" && sha256sum -c SHA256SUMS >/dev/null 2>&1) || err "контрольные суммы копии $name не сходятся — копия повреждена"
[ -s "$W/bj-data.tar.gz" ] || err "в копии $name нет данных bj"
if [ -s "$W/bj.pgdump" ]; then
pg_restore -l < "$W/bj.pgdump" >/dev/null 2>&1 || err "база bj в копии $name не читается"
elif [ -z "$(tar -xzOf "$W/bj-data.tar.gz" var/lib/bj/.bj/setup.json 2>/dev/null | python3 -c '
import json,sys,urllib.parse
try:
h=(urllib.parse.urlparse(json.load(sys.stdin).get("postgres",{}).get("dsn","")).hostname or "").lower()
print(h if h not in ("","localhost","127.0.0.1","::1") else "")
except Exception: pass')" ]; then
err "в копии $name нет базы bj"
fi # база на сервере организации, не вошедшая в копию, — её резервирует администратор баз
tar -tzf "$W/bj-data.tar.gz" >/dev/null 2>&1 || err "архив данных bj в копии $name не читается"
ok "копия $name цела: $(cd "$W" && ls | grep -vc SHA256SUMS) файлов, $(du -sh "$W" | cut -f1), контрольные суммы сходятся, база и архивы читаются" ;;
restore)
name=$(j name)
[[ "$name" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{4}$ ]] || err "не выбрана копия"
[ -n "$SHARE" ] || err "не указано хранилище"
c=$(credfile); [ -r "$c" ] || err "не указан логин и пароль хранилища"
rc=/run/bj-restore.cred; cp "$c" "$rc"; chmod 600 "$rc"
printf 'SHARE=%q\nDIR=%q\nNAME=%q\n' "$SHARE" "$DIR" "$name" > /run/bj-restore.conf; chmod 600 /run/bj-restore.conf
echo '{"stage":"start","msg":"восстановление запущено"}' > "$RSTATUS"; chown bj:bj "$RSTATUS" 2>/dev/null
systemd-run --unit="bj-restore-$(date +%s)" --collect /usr/local/sbin/bj-backup-admin restore-now >/dev/null 2>&1 || err "не удалось запустить восстановление"
ok "восстановление копии $name запущено: bj перезапустится" ;;
restore-now)
. /run/bj-restore.conf
st() { python3 - "$RSTATUS" "$1" "$2" <<'PY'
import json,sys,datetime
json.dump({"at":datetime.datetime.now().astimezone().isoformat(timespec="seconds"),"stage":sys.argv[2],"msg":sys.argv[3]},open(sys.argv[1],"w"),ensure_ascii=False)
PY
chown bj:bj "$RSTATUS" 2>/dev/null; }
die() { st error "$1"; rm -f /run/bj-restore.cred /run/bj-restore.conf; systemctl start igate bj-server >/dev/null 2>&1
for u in bj-npr bj-lk; do systemctl is-enabled "$u" >/dev/null 2>&1 && systemctl start "$u" >/dev/null 2>&1; done; exit 1; }
W=$(mktemp -d /var/tmp/bj-restore.XXXXXX)
st download "скачивание копии $NAME"
smbclient "$SHARE" -A /run/bj-restore.cred -m SMB3 -c "cd \"$DIR/$NAME\"; lcd $W; prompt; mget *" >/dev/null 2>&1 || die "не удалось скачать копию $NAME"
(cd "$W" && sha256sum -c SHA256SUMS >/dev/null 2>&1) || die "контрольные суммы копии не сходятся — копия повреждена"
[ -s "$W/bj-data.tar.gz" ] || die "в копии нет данных bj"
# база bj на сервере PostgreSQL организации (перенесена в мастере): её не восстанавливаем поверх — это база
# организации, у неё свежие данные и свой администратор; bj после восстановления настроек подключится к ней
EXTDB=$(tar -xzOf "$W/bj-data.tar.gz" var/lib/bj/.bj/setup.json 2>/dev/null | python3 -c '
import json,sys,urllib.parse
try:
h=(urllib.parse.urlparse(json.load(sys.stdin).get("postgres",{}).get("dsn","")).hostname or "").lower()
print(h if h not in ("","localhost","127.0.0.1","::1") else "")
except Exception: pass')
[ -n "$EXTDB" ] || [ -s "$W/bj.pgdump" ] || die "в копии нет базы bj"
st stop "остановка bj и шлюза НРД"
systemctl stop bj-lk bj-npr bj-server igate >/dev/null 2>&1
if [ -n "$EXTDB" ]; then
st db "база bj — на сервере организации $EXTDB: не восстанавливается, bj подключится к ней"
else
st db "восстановление базы bj"
# каталог копии доступен только root — дамп подаём postgres через stdin; сообщения — по-английски для разбора ошибок
LC_ALL=C PGOPTIONS='-c lc_messages=C' runuser -u postgres -- pg_restore --clean --if-exists -d bj < "$W/bj.pgdump" >/dev/null 2>"$W/pg.err"
# «does not exist» при --clean на пустой базе — не ошибка; остальное — ошибка
if grep -i "error" "$W/pg.err" | grep -vqi "does not exist"; then die "база bj: $(grep -i error "$W/pg.err" | grep -vi 'does not exist' | head -1)"; fi
fi
if [ -s "$W/igate.pgdump" ]; then
st db "восстановление базы шлюза НРД"
# новый сервер: ИШ ещё не ставили — готовим пользователя, роль, базу и доступ без пароля (как ставит
# bj-igate-admin), чтобы данные и каналы ИШ вернулись; сам пакет ИШ потом ставится на шаге «Шлюз НРД»
id igate >/dev/null 2>&1 || useradd --system --home-dir /var/lib/igate --create-home --shell /usr/sbin/nologin igate
runuser -u postgres -- psql -tAc "select 1 from pg_roles where rolname='igate'" | grep -q 1 || runuser -u postgres -- psql -qc "create role igate login"
runuser -u postgres -- psql -lqt | cut -d'|' -f1 | grep -qw igate || runuser -u postgres -- createdb -O igate igate
hba=$(runuser -u postgres -- psql -tAc "show hba_file")
if [ -f "$hba" ] && ! grep -q '^# bj-igate-trust' "$hba"; then
cp -a "$hba" "$hba.bak-bj-igate"
{ printf '# bj-igate-trust\nlocal igate igate trust\nhost igate igate 127.0.0.1/32 trust\nhost igate igate ::1/128 trust\n'; cat "$hba.bak-bj-igate"; } > "$hba"
runuser -u postgres -- psql -qtAc "select pg_reload_conf()" >/dev/null
fi
LC_ALL=C PGOPTIONS='-c lc_messages=C' runuser -u postgres -- pg_restore --clean --if-exists -d igate < "$W/igate.pgdump" >/dev/null 2>&1
fi
st files "восстановление данных bj и шлюза НРД"
# своё у этой машины (отпечаток железа, установленные компоненты) — сохраняем поверх копии; ключевые
# носители из старых копий не раскладываем: ключ СКЗИ — только на своём носителе
mkdir -p "$W/keep"; cp -a /var/lib/bj/.bj/components.json /etc/bj/hw-id "$W/keep/" 2>/dev/null
tar -C / -xzf "$W/bj-data.tar.gz" --exclude=etc/bj/hw-id --exclude=var/lib/bj/.bj/components.json || die "распаковка данных bj"
[ -f "$W/keep/components.json" ] && cp -a "$W/keep/components.json" /var/lib/bj/.bj/components.json
[ -f "$W/keep/hw-id" ] && cp -a "$W/keep/hw-id" /etc/bj/hw-id
# пароль пользователя базы bj — как в восстановленных настройках: установщик нового сервера задал свой
# случайный пароль, и без этого bj не подключился бы к своей базе (ушёл бы в хранение в памяти)
pw=$(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)
if u.username=="bj" and u.password and u.hostname in ("127.0.0.1","localhost"): print(urllib.parse.unquote(u.password))
except Exception: pass
PY
)
if [ -n "$pw" ]; then
printf "ALTER ROLE bj WITH PASSWORD '%s';\n" "$(printf '%s' "$pw" | sed "s/'/''/g")" | runuser -u postgres -- psql -q >/dev/null 2>&1 || die "не удалось выставить пароль базы bj из копии"
fi
[ -s "$W/igate-data.tar.gz" ] && { tar -C / -xzf "$W/igate-data.tar.gz" --exclude='var/lib/igate/keymedia/*' || die "распаковка данных шлюза НРД"; }
# копия хранилища в восстановленных настройках есть, а пароль в неё не входит — берём тот, с которым
# восстанавливали, и возвращаем расписание копий
if [ -r "$CONF" ]; then
install -m 600 /run/bj-restore.cred "$CRED"
(. "$CONF"; schedule "${EVERY_HOURS:-6}"; [ "${ENABLED:-1}" = 1 ] && systemctl enable bj-backup.timer >/dev/null 2>&1)
fi
rm -f /run/bj-restore.cred
st start "запуск служб"
systemctl start igate >/dev/null 2>&1
systemctl start bj-server >/dev/null 2>&1
systemctl is-enabled bj-backup.timer >/dev/null 2>&1 && systemctl start bj-backup.timer >/dev/null 2>&1
for u in bj-npr bj-lk; do systemctl is-enabled "$u" >/dev/null 2>&1 && systemctl start "$u" >/dev/null 2>&1; done
had_igate=0; [ -s "$W/igate-data.tar.gz" ] && had_igate=1
rm -rf "$W" /run/bj-restore.conf
if [ "$had_igate" = 1 ] && [ ! -x /opt/nsd/igate/igate-svc/igate-svc ]; then
st done "восстановлено из копии $NAME. Шлюз НРД на этом сервере не установлен — установите его в мастере (шаг «Шлюз НРД»): настройки и каналы из копии подхватятся; ключ — подключите носитель и установите мастером ключа"
else
st done "восстановлено из копии $NAME"
fi ;;
*) err "неизвестная команда: $cmd" ;;
esac