c62225dd7d
- ИШ без флешки ждёт её в окне Валидаты («Вставьте ключевой носитель»): статус «ждёт флешку», без кнопки переустановки; сторож не тратит перезапуски, а когда флешка появилась — перезапускает ИШ сам; письмо - шаг ключа: итог — карточка «Ключ подключён», «Заменить ключ» вместо «Установить ещё один», этапы свёрнуты - копии: «Копии на хранилище» ищет там же, куда ложится копия (подпапка по умолчанию bj-backup, не из старых настроек); поля показываются разобранными - [hidden] прячет всегда (пустая полоска под версией ИШ); «Шлюз НРД на другом сервере?» — что это за адрес Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
240 lines
18 KiB
Bash
Executable File
240 lines
18 KiB
Bash
Executable File
#!/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
|