#!/bin/bash # bj-igate-admin — установка и настройка интеграционного шлюза НРД (ИШ, igate) без графики. # Вызывает root-демон bj-keymedia по запросу из bj (мастер, шаг «Шлюз НРД»); параметры — JSON в BJ_EXTRA. # status — JSON: версия, служба, движок, каналы (контуры) # install — установить или обновить ИШ из дистрибутива НРД (в фоне; ход — /var/lib/bj/igate-install-status.json) # BJ_EXTRA: {"file": "<путь к igate-*.zip|.deb>"}, {"version": "107.0.930"} — скачать с nsd.ru, # или {"bundled": "1"} — дистрибутив с образа (/var/lib/bj/igate-dist), без интернета # install-now — сама установка (запускается через systemd-run) # contour — создать или обновить канал контура и перезапустить ИШ # BJ_EXTRA: {"contour": "test3|prod", "crypto": "gost|rsa", "deponent": "MC…", "profile": "<профиль Валидаты>"} # # ИШ 107 без графики (проверено 19.08.2026, см. deploy/ish/upgrade-ish-v107.sh): пароли — в gnome-keyring # (разблокируется паролем из файла), движок — под xvfb-run, сессионная шина — dbus-run-session; настройки # ИШ — в PostgreSQL (channels + parameters), канал ищется по составному коду <канал><депозитарный код>. set -uo pipefail IGATE_DIR=/opt/nsd/igate SVC_USER=igate HOME_DIR=/var/lib/igate DB=${BJ_IGATE_DB:-igate} # переопределение — только для проверки на копии базы DATA_DIR="$HOME_DIR/.local/share/NSD Integration Gateway" WRAPPER=/usr/local/bin/igate-run.sh UNIT=/etc/systemd/system/igate.service STATUS=/var/lib/bj/igate-install-status.json PARAMS=/run/bj-igate-install.json WEB=http://localhost:8090 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; } psqlq() { (cd /tmp && runuser -u postgres -- psql -d "$DB" -v ON_ERROR_STOP=1 -qtA "$@"); } # Адреса и отпечатки УЦ контуров НРД — «Обновление ИШ до версии 107» (НРД), разделы 1.1 и 1.2. contour_params() { # contour crypto → CODE|NAME|ENDPOINT|FINGERPRINT|TYPE case "$1-$2" in test3-gost) echo "TEST3|TEST3 GOST|https://gost-t3.nsd.ru/onyx-ms|EB:C2:D6:CA:73:AC:22:69:44:09:07:ED:6A:58:2C:4E:55:29:5D:00:F8:0F:1D:1E:70:A0:85:25:32:0D:DC:BF|GOST" ;; test3-rsa) echo "TEST3|TEST3 RSA|https://rsa-t3.nsd.ru/onyx-ms|5D:75:60:87:AA:1E:8D:E2:FA:F8:AF:DB:B0:43:FF:BD:E3:3C:B7:50|RSA" ;; prod-gost) echo "PROD|PROD GOST|https://edog.nsd.ru/onyx|58:C6:77:5B:20:29:AE:CA:C0:5A:1C:6D:E2:72:4B:C4:C7:CC:45:3B:EB:07:E5:D1:1E:55:0C:FB:EC:1B:D4:8A|GOST" ;; prod-rsa) echo "PROD|PROD RSA|https://edor.nsd.ru/onyx|C2:E1:6F:FB:5F:EF:2C:72:C4:F6:4A:07:24:87:B6:3F:ED:8D:DD:E9|RSA" ;; *) return 1 ;; esac } exchange_dir() { case "$1" in prod) echo "$HOME_DIR/exchange-prod" ;; *) echo "$HOME_DIR/exchange" ;; esac; } igate_version() { python3 - "$IGATE_DIR/igate-svc/igate-svc.deps.json" "$IGATE_DIR/igate/igate.deps.json" <<'PY' 2>/dev/null import json,sys for p in sys.argv[1:]: try: libs=json.load(open(p))['libraries'] for k in libs: n,_,v=k.partition('/') if n.lower() in ('igate','igate-svc','igate.svc'): print(v); sys.exit(0) except Exception: pass PY } engine_state() { curl -s -m 4 "$WEB/api/admin/engine/state" 2>/dev/null | tr -d '"'; } wait_engine() { # секунд for _ in $(seq 1 $(( $1 / 3 ))); do [ "$(engine_state)" = Running ] && return 0 sleep 3 done return 1 } run_as_igate_keyring() { # команда в сессии igate с разблокированным keyring (для igate-cli / igate-mig) local uid; uid=$(id -u "$SVC_USER") runuser -u "$SVC_USER" -- env HOME="$HOME_DIR" XDG_DATA_HOME="$HOME_DIR/.local/share" XDG_RUNTIME_DIR="/run/user/$uid" GIO_USE_VFS=local \ dbus-run-session -- bash -c 'eval "$(printf "%s\n" "$(cat "$HOME/.keyring-pass")" | gnome-keyring-daemon --replace --unlock --components=secrets,pkcs11 2>/dev/null)"; export GNOME_KEYRING_CONTROL; '"$1" } channels_json() { # каналы ИШ: код, включён, адрес НРД, ключ, крипто, депонент, папка if runuser -u postgres -- psql -lqt 2>/dev/null | cut -d'|' -f1 | grep -qw "$DB"; then psqlq -c "select coalesce(json_agg(json_build_object('id',c.id,'code',c.code,'enabled',c.enable, 'endpoint',(select value from parameters p where p.chanel_id=c.id and p.name='wslEndpoint'), 'profile',(select value from parameters p where p.chanel_id=c.id and p.name='cryptography.profile'), 'crypto',(select value from parameters p where p.chanel_id=c.id and p.name='cryptography.type'), 'deponent',(select value from parameters p where p.chanel_id=c.id and p.name='repositoryCode'), 'dir',(select value from parameters p where p.chanel_id=c.id and p.name='dir')) order by c.id),'[]') from channels c" 2>/dev/null || echo "[]" else echo "[]"; fi } case "$cmd" in status) inst=false; [ -x "$IGATE_DIR/igate-svc/igate-svc" ] && inst=true svc=$(systemctl is-active igate 2>/dev/null); st=$(engine_state) chans=$(channels_json) rb=""; BK=$(cat /var/backups/ish-last-backup.txt 2>/dev/null) [ -n "$BK" ] && [ -s "$BK/version" ] && [ -s "$BK/opt-nsd-igate.tar.gz" ] && [ -s "$BK/igate-db.dump" ] && [ -s "$BK/var-lib-igate.tar.gz" ] && rb=$(cat "$BK/version") echo "{\"ok\":true,\"installed\":$inst,\"version\":$(q "$(igate_version)"),\"service\":$(q "$svc"),\"engine\":$(q "$st"),\"rollback\":$(q "$rb"),\"channels\":${chans:-[]}}" ;; install) if pgrep -f "bj-igate-admin install-now" >/dev/null; then # идёт — не мешать; но если ход не менялся 15 минут, установка зависла: снять её и начать заново age=$(( $(date +%s) - $(stat -c %Y "$STATUS" 2>/dev/null || echo 0) )) [ "$age" -gt 900 ] || err "установка ИШ уже идёт" systemctl stop 'bj-igate-install-*' >/dev/null 2>&1; pkill -f "bj-igate-admin install-now" 2>/dev/null; sleep 2 fi file=$(j file); ver=$(j version) [ "$(j bundled)" = 1 ] && [ -z "$file$ver" ] && { file=$(ls /var/lib/bj/igate-dist/igate-*-amd64.zip 2>/dev/null | sort -V | tail -1); [ -n "$file" ] || err "в образе нет дистрибутива ИШ"; BJ_EXTRA="{\"file\":\"$file\"}"; } [ -n "$file$ver" ] || err "не указан дистрибутив ИШ (файл или версия)" [ -z "$file" ] || [ -f "$file" ] || err "файл дистрибутива не найден: $file" [[ -z "$ver" || "$ver" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || err "версия ИШ — вида 107.0.930" (umask 077; printf '%s' "${BJ_EXTRA:-}" > "$PARAMS") echo '{"stage":"queued","msg":"установка запущена"}' > "$STATUS"; chown bj:bj "$STATUS" 2>/dev/null # не дольше 40 минут: что бы ни зависло, служба установки будет остановлена (ход покажет «прервалась») systemd-run --unit="bj-igate-install-$(date +%s)" --collect -p RuntimeMaxSec=2400 /usr/local/sbin/bj-igate-admin install-now >/dev/null 2>&1 || err "не удалось запустить установку" ok "установка ИШ запущена" ;; install-now) BJ_EXTRA=$(cat "$PARAMS" 2>/dev/null); export BJ_EXTRA="${BJ_EXTRA:-{\}}"; rm -f "$PARAMS" LOG=/var/log/bj-igate-install.log; : > "$LOG" st() { python3 - "$STATUS" "$1" "$2" "$LOG" <<'PY' import json,sys,datetime tail=open(sys.argv[4],errors='replace').read()[-1500:] json.dump({"at":datetime.datetime.now().astimezone().isoformat(timespec="seconds"),"stage":sys.argv[2],"msg":sys.argv[3],"log":tail},open(sys.argv[1],"w"),ensure_ascii=False) PY chown bj:bj "$STATUS" 2>/dev/null; echo "== $2" >> "$LOG"; } die() { st error "$1"; exit 1; } exec >>"$LOG" 2>&1 W=$(mktemp -d /var/tmp/bj-igate.XXXXXX); trap 'rm -rf "$W"' EXIT st deps "системные пакеты для работы ИШ без графики" need="" for p in gnome-keyring dbus xvfb xauth libsecret-tools unzip curl; do dpkg -s "$p" >/dev/null 2>&1 || need="$need $p"; done if [ -n "$need" ]; then DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends $need || \ { DEBIAN_FRONTEND=noninteractive apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends $need; } || die "не удалось установить пакеты:$need" fi st dist "дистрибутив ИШ" file=$(j file); ver=$(j version) if [ -z "$file" ]; then v=$(printf '%s' "$ver" | tr . -) url="https://www.nsd.ru/media/integratsionnyj-shlyuz/$v/igate-$v-amd64.zip" echo "скачивание $url" curl -fsSL -m 600 -A 'Mozilla/5.0 (X11; Linux x86_64) bj-installer' -H 'Accept: */*' -o "$W/igate.zip" "$url" || die "не удалось скачать дистрибутив ИШ $ver с сайта НРД ($url)" file="$W/igate.zip" fi case "$file" in *.deb) deb="$file" ;; *) unzip -o -q "$file" -d "$W/x" || die "архив дистрибутива не распаковывается"; deb=$(find "$W/x" -name 'igate_*.deb' | head -1) ;; esac [ -n "$deb" ] && [ -f "$deb" ] || die "в дистрибутиве нет пакета igate_*.deb" echo "пакет $(basename "$deb"), версия $(dpkg-deb -f "$deb" Version), sha256 $(sha256sum "$deb" | cut -c1-16)…" st user "пользователь igate и хранилище паролей" id "$SVC_USER" >/dev/null 2>&1 || useradd --system --home-dir "$HOME_DIR" --create-home --shell /usr/sbin/nologin "$SVC_USER" || die "не создать пользователя igate" install -d -o "$SVC_USER" -g "$SVC_USER" -m 755 "$HOME_DIR" "$HOME_DIR/.local" "$HOME_DIR/.local/share" "$DATA_DIR" "$HOME_DIR/profiles" "$HOME_DIR/.Validata" "$(exchange_dir test3)" uid=$(id -u "$SVC_USER") loginctl enable-linger "$SVC_USER" >/dev/null 2>&1 || true [ -d "/run/user/$uid" ] || { install -d -o "$SVC_USER" -g "$SVC_USER" -m 700 "/run/user/$uid"; } if [ ! -s "$HOME_DIR/.keyring-pass" ]; then (umask 077; head -c 24 /dev/urandom | base64 | tr -dc 'A-Za-z0-9' | head -c 20 > "$HOME_DIR/.keyring-pass") chown "$SVC_USER:$SVC_USER" "$HOME_DIR/.keyring-pass"; chmod 600 "$HOME_DIR/.keyring-pass" fi [ -f "$HOME_DIR/.Validata/zpki.ini" ] || { printf '[Profile]\nCount = 0\n' > "$HOME_DIR/.Validata/zpki.ini"; chown "$SVC_USER:$SVC_USER" "$HOME_DIR/.Validata/zpki.ini"; } st db "база ИШ в PostgreSQL" runuser -u postgres -- psql -tAc "select 1 from pg_roles where rolname='$SVC_USER'" | grep -q 1 || runuser -u postgres -- psql -qc "create role $SVC_USER login" || die "не создать роль igate" runuser -u postgres -- psql -lqt | cut -d'|' -f1 | grep -qw "$DB" || runuser -u postgres -- createdb -O "$SVC_USER" "$DB" || die "не создать базу igate" hba=$(runuser -u postgres -- psql -tAc "show hba_file") if [ -f "$hba" ] && ! grep -q '^# bj-igate-trust' "$hba"; then # ИШ хранит пароль БД зашифрованным — локальному igate доступ без пароля (только своя база, только с этого сервера) cp -a "$hba" "$hba.bak-bj-igate" { printf '# bj-igate-trust\nlocal %s %s trust\nhost %s %s 127.0.0.1/32 trust\nhost %s %s ::1/128 trust\n' "$DB" "$SVC_USER" "$DB" "$SVC_USER" "$DB" "$SVC_USER"; cat "$hba.bak-bj-igate"; } > "$hba" runuser -u postgres -- psql -qtAc "select pg_reload_conf()" >/dev/null fi oldv=""; [ -d "$IGATE_DIR" ] && oldv=$(igate_version) st stop "остановка ${oldv:+старого }ИШ${oldv:+ $oldv}" systemctl stop igate >/dev/null 2>&1 || true pkill -9 -u "$SVC_USER" -f igate-svc 2>/dev/null; pkill -9 -u "$SVC_USER" -f Xvfb 2>/dev/null; rm -f /tmp/CoreFxPipe_NSD.IGate.* if [ -d "$IGATE_DIR" ]; then st backup "резервная копия ИШ $oldv (программа, данные, база)" BK=/var/backups/ish-$(date +%Y%m%d-%H%M%S); mkdir -p "$BK" channels_json > "$BK/channels.json" # для проверки после обновления: каналы и ключи те же tar czf "$BK/opt-nsd-igate.tar.gz" -C "$(dirname "$IGATE_DIR")" "$(basename "$IGATE_DIR")" 2>/dev/null runuser -u postgres -- pg_dump -Fc "$DB" > "$BK/igate-db.dump" 2>/dev/null # для отката: данные ИШ (без флешки с ключом и кэша), юнит, обёртка запуска, версия tar czf "$BK/var-lib-igate.tar.gz" --exclude="${HOME_DIR#/}/keymedia" --exclude="${HOME_DIR#/}/.cache" -C / "${HOME_DIR#/}" 2>/dev/null cp -a "$UNIT" "$BK/" 2>/dev/null; cp -a "$UNIT.d" "$BK/" 2>/dev/null; cp -a "$WRAPPER" "$BK/" 2>/dev/null igate_version > "$BK/version" echo "$BK" > /var/backups/ish-last-backup.txt; echo "резервная копия прежнего ИШ $(cat "$BK/version"): $BK" [ -s "$BK/var-lib-igate.tar.gz" ] && [ -s "$BK/igate-db.dump" ] && [ -s "$BK/opt-nsd-igate.tar.gz" ] && FULLBK=$BK fi if [ -n "${FULLBK:-}" ]; then # обновление поверх работающего ИШ: при любой ошибке дальше — вернуть прежнюю версию (этап rollback, не error: # страница продолжает показывать ход, итог — ошибка с пометкой «откатан») newv=$(basename "$deb" | sed -E "s/^igate_([0-9.]+)-([0-9]+)_.*/\\1.\\2/") # igate_110.0-997_amd64.deb → 110.0.997 die() { st rollback "$1 — откатываю к прежней версии"; echo "== ошибка обновления: $1 — откат"; BJ_RB_REASON="$1" BJ_RB_TO="$newv" /usr/local/sbin/bj-igate-admin rollback-now; exit 1; } fi st install "установка ${FULLBK:+нового }ИШ${newv:+ $newv}" echo "igate igate/service boolean false" | debconf-set-selections echo "igate igate/shortcut boolean false" | debconf-set-selections DEBIAN_FRONTEND=noninteractive dpkg -i --force-confnew "$deb" || { DEBIAN_FRONTEND=noninteractive apt-get -f install -y -qq || die "пакет ИШ не установился (dpkg)"; } [ -x "$IGATE_DIR/igate-svc/igate-svc" ] || die "после установки нет $IGATE_DIR/igate-svc/igate-svc" st config "настройка ИШ для работы без графики" cfg='{"db":{"databaseProvider":"Postgresql","sqlite":{"path":"'"$DATA_DIR"'/igate.db.dat"},"postgresql":{"host":"127.0.0.1","port":"5432","database":"'"$DB"'","userName":"'"$SVC_USER"'","extensions":null}},"migrationCompleted":true}' for f in "$DATA_DIR/config.json" "$HOME_DIR/config.json"; do [ -s "$f" ] || { printf '%s\n' "$cfg" > "$f"; chown "$SVC_USER:$SVC_USER" "$f"; chmod 600 "$f"; } done cat > "$WRAPPER" </dev/null)" export GNOME_KEYRING_CONTROL SSH_AUTH_SOCK cd "\$HOME" exec xvfb-run -a $IGATE_DIR/igate-svc/igate-svc EOF chmod 755 "$WRAPPER" rm -f /etc/systemd/system/igate.service.d/override.conf cat > "$UNIT" </dev/null; true' ExecStart=/usr/bin/dbus-run-session -- $WRAPPER Restart=on-failure RestartSec=8 [Install] WantedBy=multi-user.target EOF systemctl daemon-reload st schema "база ИШ: создание схемы" if ! psqlq -c "select to_regclass('public.parameters') is not null" | grep -q t; then # -k: .NET-программа может не завершиться по SIGTERM — через 15 с принудительно (иначе ждали бы бесконечно) timeout -k 15 150 bash -c "$(declare -f run_as_igate_keyring); SVC_USER=$SVC_USER HOME_DIR=$HOME_DIR run_as_igate_keyring 'timeout -k 15 90 $IGATE_DIR/igate-cli/igate-cli --data $HOME_DIR /dev/null 2>&1; true'" || true psqlq -c "select to_regclass('public.parameters') is not null" | grep -q t || die "ИШ не создал схему базы (igate-cli). Журнал: $LOG" fi # Общие параметры ИШ. Обязательные для работы без графики и для bj (движок при старте, Web API на # localhost:8090 без авторизации) — выставляем всегда; остальные — только если их ещё нет: настройки ИШ, # который у клиента уже работает (очистка, хранение, прокси), не трогаем. Значения по умолчанию — эталон GUI. psqlq <<'SQL' || die "не записаны параметры ИШ" CREATE TEMP TABLE must(n text, v text); INSERT INTO must VALUES ('runEngineOnStartApp','True'),('server.useServer','True'),('server.host','localhost'), ('server.port','8090'),('server.scheme','Http'),('server.authentication.enable','False'); CREATE TEMP TABLE dflt(n text, v text); INSERT INTO dflt VALUES ('server.authentication.userName',''),('server.authentication.password',''),('server.certificate.storage','File'), ('server.certificate.store.location','CurrentUser'),('server.certificate.store.name','My'),('server.certificate.file.path',''), ('server.certificate.file.password',''),('wsl.httpsMode','Auto'),('wsl.maxConnsPerServer','4'),('wsl.proxy.mode','None'), ('wsl.proxy.address',''),('wsl.proxy.port','0'),('wsl.proxy.username',''),('wsl.proxy.password',''),('enableDbLogging','False'), ('cleanAutomatically','False'),('cleanAtTime','00:30:00'),('cleanWhenLarger','1024'),('cleanVacuum','False'),('storePeriod','15'), ('archiveAutomatically','False'); UPDATE parameters p SET value = m.v FROM must m WHERE p.chanel_id IS NULL AND p.name = m.n; INSERT INTO parameters(name, value, chanel_id) SELECT x.n, x.v, NULL FROM (SELECT * FROM must UNION ALL SELECT * FROM dflt) x WHERE NOT EXISTS (SELECT 1 FROM parameters p WHERE p.chanel_id IS NULL AND p.name = x.n); SQL st start "запуск ИШ" systemctl enable igate >/dev/null 2>&1 systemctl restart igate || die "служба igate не запустилась" wait_engine 120 || die "ИШ запущен, но движок не перешёл в Running за 2 минуты. Журнал ИШ: $DATA_DIR/logs-svc/log.txt" case "$(j file)" in /var/lib/bj/igate-upload/*) rm -f "$(j file)" ;; esac # загруженный архив больше не нужен (с образа — остаётся) if [ -n "${FULLBK:-}" ]; then # обновление: «готово» — только когда новый ИШ проверен; не прошло — die (автооткат к прежней версии) st verify "проверка нового ИШ: версия, служба, каналы, ключ, обмен с НРД" v=$(igate_version) [ "$v" = "$newv" ] || die "после установки версия ИШ $v, ожидалась $newv" echo "✓ версия ИШ $v" sleep 10 [ "$(systemctl is-active igate)" = active ] && [ "$(engine_state)" = Running ] || die "ИШ $v остановился после запуска" echo "✓ служба igate работает, движок Running" now=$(channels_json) chk=$(python3 - "$FULLBK/channels.json" "$now" <<'PY' import json,sys keys=('code','enabled','endpoint','profile','crypto','deponent','dir') try: old=json.load(open(sys.argv[1])) except Exception: old=None new=json.loads(sys.argv[2] or '[]') pick=lambda l:[{k:c.get(k) for k in keys} for c in l] if old is not None and pick(old)!=pick(new): print('ERR каналы ИШ после обновления не совпадают с прежними: было %s, стало %s'%(pick(old),pick(new))); sys.exit() on=[c for c in new if c.get('enabled')] print('OK '+'; '.join('канал %s включён, ключ %s'%(c['code'],c.get('profile') or '—') for c in on) if on else 'OK каналов не настроено') PY ) case "$chk" in OK*) echo "✓ ${chk#OK } — как до обновления" ;; *) die "${chk#ERR }" ;; esac xok="" if printf '%s' "$now" | grep -q '"enabled" : true'; then pid=$(pgrep -u "$SVC_USER" -f "$IGATE_DIR/igate-svc/igate-svc" | tail -1) for _ in $(seq 1 30); do grep -a "PID_$pid " "$DATA_DIR/logs-svc/log.txt" 2>/dev/null | grep -q -- '-> OK (HTTP 200)' && { xok=1; break; } sleep 3 done # НРД не ответил — не повод откатывать (старая версия связаться тоже не сможет): итог с предупреждением [ -n "$xok" ] && echo "✓ обмен с НРД: запрос к НРД прошёл (HTTP 200)" || echo "⚠ за 90 с ИШ не получил ответа НРД — проверьте связь" fi st done "ИШ обновлён с $oldv до $v и проверен: служба и движок работают, каналы и ключ как до обновления${xok:+, обмен с НРД идёт}${xok:-; ответа НРД пока нет — проверьте связь}" else st done "ИШ $(igate_version) установлен и работает; дальше — канал контура (TEST3 или боевой)" fi ;; rollback) pgrep -f "bj-igate-admin (install|rollback)-now" >/dev/null && err "установка или откат ИШ уже идёт" BK=$(cat /var/backups/ish-last-backup.txt 2>/dev/null) [ -n "$BK" ] && [ -s "$BK/version" ] && [ -s "$BK/var-lib-igate.tar.gz" ] || err "нет полной резервной копии прежнего ИШ — откатить нельзя" echo '{"stage":"queued","msg":"откат запущен"}' > "$STATUS"; chown bj:bj "$STATUS" 2>/dev/null systemd-run --unit="bj-igate-rollback-$(date +%s)" --collect -p RuntimeMaxSec=1800 /usr/local/sbin/bj-igate-admin rollback-now >/dev/null 2>&1 || err "не удалось запустить откат" ok "откат ИШ к $(cat "$BK/version") запущен" ;; rollback-now) LOG=/var/log/bj-igate-install.log; [ -n "${BJ_RB_REASON:-}" ] || : > "$LOG" st() { python3 - "$STATUS" "$1" "$2" "$LOG" <<'PY' import json,sys,datetime tail=open(sys.argv[4],errors='replace').read()[-1500:] json.dump({"at":datetime.datetime.now().astimezone().isoformat(timespec="seconds"),"stage":sys.argv[2],"msg":sys.argv[3],"log":tail},open(sys.argv[1],"w"),ensure_ascii=False) PY chown bj:bj "$STATUS" 2>/dev/null; echo "== $2" >> "$LOG"; } die() { st error "${BJ_RB_REASON:+Обновление ИШ не прошло ($BJ_RB_REASON), и откат не удался: }$1"; exit 1; } exec >>"$LOG" 2>&1 BK=$(cat /var/backups/ish-last-backup.txt 2>/dev/null); old=$(cat "$BK/version" 2>/dev/null); cur=$(igate_version) [ -n "$old" ] || die "нет резервной копии прежнего ИШ" st stop "остановка ИШ $cur" systemctl stop igate >/dev/null 2>&1 || true pkill -9 -u "$SVC_USER" -f igate-svc 2>/dev/null; pkill -9 -u "$SVC_USER" -f Xvfb 2>/dev/null; rm -f /tmp/CoreFxPipe_NSD.IGate.* st files "возврат программы и данных ИШ $old" rm -rf "$IGATE_DIR" && tar xzf "$BK/opt-nsd-igate.tar.gz" -C "$(dirname "$IGATE_DIR")" || die "не распаковалась программа ИШ из $BK" find "$HOME_DIR" -mindepth 1 -maxdepth 1 ! -name keymedia ! -name .cache -exec rm -rf {} + 2>/dev/null tar xzf "$BK/var-lib-igate.tar.gz" -C / || die "не распаковались данные ИШ из $BK" [ -f "$BK/$(basename "$UNIT")" ] && cp -a "$BK/$(basename "$UNIT")" "$UNIT" [ -d "$BK/$(basename "$UNIT").d" ] && { rm -rf "$UNIT.d"; cp -a "$BK/$(basename "$UNIT").d" "$UNIT.d"; } [ -f "$BK/$(basename "$WRAPPER")" ] && cp -a "$BK/$(basename "$WRAPPER")" "$WRAPPER" st db "возврат базы ИШ" # текущие счётчики id: после отката не опускать их ниже — иначе ИШ выдаст новым пакетам уже виденные bj номера seqs=$(psqlq -c "select sequencename||' '||coalesce(last_value,0) from pg_sequences where schemaname='public'" 2>/dev/null) runuser -u postgres -- dropdb --if-exists "$DB" && runuser -u postgres -- createdb -O "$SVC_USER" "$DB" || die "не пересоздана база ИШ" runuser -u postgres -- pg_restore -d "$DB" "$BK/igate-db.dump" || true psqlq -c "select to_regclass('public.parameters') is not null" | grep -q t || die "база ИШ не восстановилась из $BK/igate-db.dump" printf '%s\n' "$seqs" | while read -r n v; do [ -n "$n" ] && psqlq -c "select setval('public.\"$n\"', greatest($v, (select last_value from public.\"$n\")))" >/dev/null 2>&1 done # пришедший с обновлением дистрибутив больше не ставить сам (иначе bj вернёт версию сразу); новая версия — снова автоматически dv=$(python3 -c "import json;print(json.load(open('/var/lib/bj/.bj/components.json')).get('igate-dist',{}).get('version',''))" 2>/dev/null) [ -n "$dv" ] && { printf '%s ' "$dv" > /var/lib/bj/igate-autoupgrade-skip; chown bj:bj /var/lib/bj/igate-autoupgrade-skip 2>/dev/null; } st start "запуск ИШ $old" systemctl daemon-reload; systemctl restart igate || die "служба igate не запустилась" wait_engine 120 || die "ИШ $old запущен, но движок не перешёл в Running за 2 минуты. Журнал ИШ: $DATA_DIR/logs-svc/log.txt" if [ -n "${BJ_RB_REASON:-}" ]; then st error "Обновление ИШ${BJ_RB_TO:+ до $BJ_RB_TO} не прошло: $BJ_RB_REASON. ИШ автоматически откатан к $old и работает; настройки и каналы прежние." else st done "ИШ откатан с $cur к $old и работает" fi ;; contour) contour=$(j contour); crypto=$(j crypto); dep=$(j deponent); prof=$(j profile) crypto=${crypto:-gost} IFS='|' read -r CODE NAME EP FP TYPE <<<"$(contour_params "$contour" "$crypto")" || true [ -n "${CODE:-}" ] || err "неизвестный контур: $contour/$crypto" [[ "$dep" =~ ^[A-Z]{2}[0-9]{10}$ ]] || err "депозитарный код — вида MC0000000000" [[ -z "$prof" || "$prof" =~ ^[A-Za-z0-9_-]+$ ]] || err "недопустимое имя профиля Валидаты" [ -x "$IGATE_DIR/igate-svc/igate-svc" ] || err "ИШ не установлен" # ключ этого контура ставили раньше, чем канал: профиль запомнен установкой ключа if [ -z "$prof" ] && [ -s "$HOME_DIR/.bj-profile-$CODE" ]; then prof=$(tr -cd 'A-Za-z0-9_-' < "$HOME_DIR/.bj-profile-$CODE"); fi DIR=$(exchange_dir "$contour"); install -d -o "$SVC_USER" -g "$SVC_USER" -m 755 "$DIR" psqlq -v code="$CODE$dep" -v name="$NAME" -v ep="$EP" -v fp="$FP" -v typ="$TYPE" -v dep="$dep" -v prof="$prof" -v dir="$DIR" <<'SQL' || err "канал не записан в базу ИШ" BEGIN; INSERT INTO channels(name, code, type, enable) SELECT :'name', :'code', 'WSL', true WHERE NOT EXISTS (SELECT 1 FROM channels WHERE code = :'code'); CREATE TEMP TABLE want(n text, v text) ON COMMIT DROP; INSERT INTO want VALUES ('enable','True'),('wslEndpoint',:'ep'),('cryptography.type',:'typ'),('cryptography.pincode',NULL), ('cryptography.clientCertificateSerialNumber',''),('repositoryCode',:'dep'),('fetchInterval','00:00:10'),('attemptInterval','30000'), ('sendAttempts','3'),('maxPartSize','500'),('loadOldMessagesDepth','3'),('isIncomingEnabled','True'),('isOutgoingEnabled','True'), ('isTransitTerminalChannel','False'),('useDirectories','True'),('dir',:'dir'),('inboxDirName','INBOX'),('outboxDirName','OUTBOX'), ('sentDirName','SENT'),('errorDirName','ERRORS'),('archive1042sDirName','Archives1042S'),('enableLockFile','True'), ('enableAutoResponse','True'),('enable1042ReportProcessing','True'),('RenameOutgoingFiles','True'),('generateReceivedPackageInfo','True'), ('generateSentPackageInfo','False'),('moveReceiptsToSentFolder','False'),('applyAddHashOfPackageToFolder','False'), ('ignorePackageDirectoryStructure','True'),('checkReceivedPackageNsdSign','False'),('checkReceivedPackageSenderSign','False'), ('autoUpdateTransitMember','False'),('automaticcalyLoadCrls','False'),('autoInPkgReportOffload','False'),('monitoringThreshold','00:00:01'), ('autoLoadCrlsTime',NULL),('fetchThreadCount',NULL),('forceCryPackageEncryption',NULL),('inPkgReportDirectory',NULL), ('inPkgReportOffloadInterval',NULL),('maxPackagesPerJob',NULL),('nsdCertificateSerialNumbers',''),('pkiDecryptMode',NULL),('pkiEncryptMode',NULL), ('pkiSignMode',NULL),('pkiVerifyMode',NULL),('receiveProcThreadCount',NULL),('sendProcThreadCount',NULL),('updateTransitMemberListTime',NULL), ('wslTlsCACertFingerprint',:'fp'),('wslTlsCertDN','*.nsd.ru'),('wslTlsCertChainLen','2'),('isM2M','True'); -- Необязательные параметры канала — NULL, как их пишет интерфейс ИШ. Пустую строку ИШ разбирает как число или -- интервал, падает и молча выкидывает канал («There are no active channels», API: «more than one Channel with same -- code») — так было на чистом сервере .29 (06.10.2026). Каналы, записанные прежними версиями bj, чинятся здесь же. UPDATE parameters SET value = NULL WHERE chanel_id IS NOT NULL AND value = '' AND name IN ('autoLoadCrlsTime','cryptography.pincode','fetchThreadCount', 'forceCryPackageEncryption','inPkgReportDirectory','inPkgReportOffloadInterval','maxPackagesPerJob','pkiDecryptMode','pkiEncryptMode', 'pkiSignMode','pkiVerifyMode','receiveProcThreadCount','sendProcThreadCount','updateTransitMemberListTime'); UPDATE parameters SET value = 'Archives1042S' WHERE chanel_id IS NOT NULL AND name = 'archive1042sDirName' AND value LIKE '/%/Archives1042S'; -- существующие значения канала не трогаем, кроме контурных (адрес, отпечаток, криптография, депозитарный код) UPDATE parameters p SET value = w.v FROM want w, channels c WHERE c.code = :'code' AND p.chanel_id = c.id AND p.name = w.n AND w.n IN ('wslEndpoint','wslTlsCACertFingerprint','wslTlsCertDN','wslTlsCertChainLen','cryptography.type','repositoryCode','isM2M'); INSERT INTO parameters(name, value, chanel_id) SELECT w.n, w.v, c.id FROM want w, channels c WHERE c.code = :'code' AND NOT EXISTS (SELECT 1 FROM parameters p WHERE p.chanel_id = c.id AND p.name = w.n); -- профиль Валидаты (ключ этого контура): задан — пишем; не задан — оставляем прежний или пустой до установки ключа INSERT INTO parameters(name, value, chanel_id) SELECT 'cryptography.profile', '', c.id FROM channels c WHERE c.code = :'code' AND NOT EXISTS (SELECT 1 FROM parameters p WHERE p.chanel_id = c.id AND p.name = 'cryptography.profile'); UPDATE parameters p SET value = :'prof' FROM channels c WHERE :'prof' <> '' AND c.code = :'code' AND p.chanel_id = c.id AND p.name = 'cryptography.profile'; -- канал включён, только когда для контура установлен ключ (профиль Валидаты): без ключа ИШ его не поднимет UPDATE channels c SET enable = COALESCE((SELECT value <> '' FROM parameters p WHERE p.chanel_id = c.id AND p.name = 'cryptography.profile'), false) WHERE c.code = :'code'; UPDATE parameters p SET value = CASE WHEN c.enable THEN 'True' ELSE 'False' END FROM channels c WHERE c.code = :'code' AND p.chanel_id = c.id AND p.name = 'enable'; COMMIT; SQL en=$(psqlq -v code="$CODE$dep" <<<"select enable from channels where code = :'code'") if [ -n "${BJ_IGATE_NORESTART:-}" ]; then ok "канал $CODE$dep записан (без перезапуска, enable=$en)"; exit 0; fi systemctl restart igate >/dev/null 2>&1 || err "служба igate не перезапустилась" if wait_engine 120; then if [ "$en" = t ]; then ok "канал $CODE$dep ($NAME, $EP) настроен и включён, ИШ работает" else ok "канал $CODE$dep ($NAME, $EP) записан и пока выключен — включится после установки ключа этого контура"; fi else err "канал $CODE$dep записан, но движок ИШ не перешёл в Running — см. журнал ИШ $DATA_DIR/logs-svc/log.txt (отпечаток УЦ, адрес контура, ключ)" fi ;; # папки обмена шлюза по сети (SMB): для программ организации, которые кладут/забирают файлы через ИШ. bj это не # нужно. Блок общих папок в smb.conf — между метками bj; заведённые вручную [igate_exchange] не трогаем. share) # Папки обмена шлюза по сети (Samba). Два режима: # simple — один логин и пароль (пользователь Samba, файлы пишутся от имени шлюза); # domain — сервер в домене AD (winbind): входят сотрудники и группы домена с доступом к bj (список даёт bj). CONF=/etc/samba/smb.conf; B='# >>> bj: папки шлюза НРД'; E='# <<< bj: папки шлюза НРД' GB='# >>> bj: вход через домен'; GE='# <<< bj: вход через домен' on=$(j enabled); su=$(j user); sp=$(j password); mode=$(j mode); vu=$(j valid) manual=false; [ -f "$CONF" ] && sed "/^$B/,/^$E/d" "$CONF" | grep -qE '^\[igate_exchange(_test|_prod)?\]' && manual=true dmode=simple; [ -f "$CONF" ] && grep -q "^$GB" "$CONF" && dmode=domain if [ "${on:-}" = status ] || [ -z "${on:-}" ]; then mine=false; [ -f "$CONF" ] && grep -q "^$B" "$CONF" && mine=true u=$( [ "$mine" = true ] && sed -n "/^$B/,/^$E/p" "$CONF" | sed -n 's/^ *valid users *= *//p' | head -1 ) [ "$manual" = true ] && [ -z "$u" ] && u=$(sed -n '/^\[igate_exchange\]/,/^\[/p' "$CONF" | sed -n 's/^ *valid users *= *//p' | head -1) # куда подключаться из проводника: адрес и имя сервера, открытые папки (test3 / prod) ip=$(hostname -I 2>/dev/null | awk '{print $1}'); hn=$(hostname -s 2>/dev/null) sh=""; [ "$mine" = true ] && for n in $(sed -n "/^$B/,/^$E/p" "$CONF" | sed -n 's/^\[\(.*\)\]$/\1/p'); do sh="$sh${sh:+,}$(q "$n")"; done [ "$manual" = true ] && [ -z "$sh" ] && sh=$(q igate_exchange) realm=$( [ "$dmode" = domain ] && sed -n "/^$GB/,/^$GE/p" "$CONF" | sed -n 's/^ *realm *= *//p' | head -1 ) joined=false; [ "$dmode" = domain ] && command -v net >/dev/null 2>&1 && net ads testjoin >/dev/null 2>&1 && joined=true echo "{\"ok\":true,\"on\":$( [ "$mine" = true ] || [ "$manual" = true ] && echo true || echo false),\"manual\":$manual,\"mode\":$(q "$dmode"),\"joined\":$joined,\"realm\":$(q "$realm"),\"user\":$(q "$u"),\"smbd\":$(q "$(systemctl is-active smbd 2>/dev/null)"),\"ip\":$(q "$ip"),\"host\":$(q "$hn"),\"shares\":[$sh]}"; exit 0 fi [ "$manual" = true ] && err "папка igate_exchange уже открыта вручную в $CONF — bj её не меняет; чтобы управлять из bj, уберите её из smb.conf" if [ "$on" != true ]; then [ -f "$CONF" ] && sed -i "/^$B/,/^$E/d" "$CONF" systemctl disable --now smbd nmbd >/dev/null 2>&1 || true # папок bj нет — сервер не отвечает по SMB ok "доступ к папкам шлюза по сети закрыт, Samba выключена"; exit 0 fi [ -n "$mode" ] || mode=$dmode if ! command -v smbd >/dev/null 2>&1; then DEBIAN_FRONTEND=noninteractive apt-get install -y -q samba >/dev/null 2>&1 || err "не удалось установить Samba (apt-get install samba) — нужен доступ к репозиторию" fi note="" if [ "$mode" = domain ]; then # подключение сервера к домену — один раз: учётная запись администратора домена не сохраняется if [ "$(j join)" = 1 ]; then realm=$(j realm | tr '[:lower:]' '[:upper:]'); wg=$(j workgroup | tr '[:lower:]' '[:upper:]'); au=$(j admin); ap=$(j adminpw) [[ "$realm" =~ ^[A-Z0-9][A-Z0-9.-]+$ ]] || err "домен не задан — подключите домен в bj: Настройка → Пользователи" [[ "$wg" =~ ^[A-Z0-9][A-Z0-9_-]{0,14}$ ]] || err "не определено краткое имя домена (NetBIOS)" [ -n "$au" ] && [ -n "$ap" ] || err "нужны логин и пароль администратора домена" command -v winbindd >/dev/null 2>&1 || DEBIAN_FRONTEND=noninteractive apt-get install -y -q winbind libnss-winbind >/dev/null 2>&1 || err "не удалось установить winbind (apt-get install winbind) — нужен доступ к репозиторию" cp -p "$CONF" "$CONF.bj-before-join" 2>/dev/null sed -i "/^$GB/,/^$GE/d" "$CONF" { echo "$GB" printf '[global]\n workgroup = %s\n realm = %s\n security = ads\n kerberos method = secrets and keytab\n winbind refresh tickets = yes\n idmap config * : backend = tdb\n idmap config * : range = 3000-7999\n idmap config %s : backend = rid\n idmap config %s : range = 10000-999999\n map to guest = never\n' "$wg" "$realm" "$wg" "$wg" echo "$GE"; } >> "$CONF" # блок домена — раньше папок: папки дописываются после него if grep -q "^$B" "$CONF"; then blk=$(sed -n "/^$B/,/^$E/p" "$CONF"); sed -i "/^$B/,/^$E/d" "$CONF"; printf '%s\n' "$blk" >> "$CONF"; fi testparm -s "$CONF" >/dev/null 2>&1 || { cp -p "$CONF.bj-before-join" "$CONF"; err "smb.conf с параметрами домена не проходит проверку (testparm) — правка отменена"; } out=$(PASSWD="$ap" net ads join -U "$au" 2>&1) || { cp -p "$CONF.bj-before-join" "$CONF"; systemctl restart smbd >/dev/null 2>&1; err "сервер не подключён к домену: $(printf '%s' "$out" | tail -2 | tr '\n' ' ')"; } systemctl enable winbind >/dev/null 2>&1; systemctl restart winbind >/dev/null 2>&1 net ads testjoin >/dev/null 2>&1 || err "сервер подключён, но проверка подключения к домену не прошла (net ads testjoin)" note="сервер подключён к домену $realm; "; dmode=domain fi [ "$dmode" = domain ] || err "сервер ещё не подключён к домену — нажмите «Подключить сервер к домену»" [ -n "$vu" ] || err "в bj нет групп и сотрудников домена с доступом — добавьте их в Настройка → Пользователи" case "$vu" in *$'\n'*|*'['*|*']'*|*';'*|*'#'*|*'%'*) err "недопустимые символы в списке доступа" ;; esac valid="$vu" else [[ "$su" =~ ^[a-z][a-z0-9_-]{2,31}$ ]] || err "логин — латинские строчные буквы, цифры, _ и -, от 3 символов, начинается с буквы" case "$su" in root|igate|bj|postgres) err "логин $su занят системой — выберите другой" ;; esac id "$su" >/dev/null 2>&1 || useradd --system --no-create-home --shell /usr/sbin/nologin "$su" || err "пользователь $su не создан" if [ -n "$sp" ]; then [ ${#sp} -ge 8 ] || err "пароль — не короче 8 символов" printf '%s\n%s\n' "$sp" "$sp" | smbpasswd -s -a "$su" >/dev/null 2>&1 || err "пароль для $su не задан (smbpasswd)" smbpasswd -e "$su" >/dev/null 2>&1 elif ! pdbedit -L 2>/dev/null | grep -q "^$su:"; then err "задайте пароль для $su" fi # простой режим — без домена: вход учётной записью Samba if [ "$dmode" = domain ]; then sed -i "/^$GB/,/^$GE/d" "$CONF"; systemctl disable --now winbind >/dev/null 2>&1; note="вход через домен выключен (учётная запись компьютера в домене осталась); "; fi valid="$su" fi [ -f "$CONF" ] && sed -i "/^$B/,/^$E/d" "$CONF" { echo "$B" for c in test3 prod; do d=$(exchange_dir "$c"); [ -d "$d" ] || continue n=igate_exchange_test; [ "$c" = prod ] && n=igate_exchange # боевой — без суффикса, тестовый — _test printf '[%s]\n comment = Шлюз НРД: папки обмена %s (INBOX/OUTBOX/SENT/ERRORS)\n path = %s\n browseable = yes\n read only = no\n valid users = %s\n force user = %s\n force group = %s\n' "$n" "$c" "$d" "$valid" "$SVC_USER" "$SVC_USER" done echo "$E"; } >> "$CONF" # Windows сначала пробует войти текущей учётной записью; с «map to guest = bad user» Samba пускает её гостем, # гостю в папку нельзя — и Windows пишет «нет доступа» вместо окна логина и пароля. С «never» — спрашивает пароль. sed -i -E 's/^([[:space:]]*)map to guest[[:space:]]*=[[:space:]]*bad user.*$/\1map to guest = never/I' "$CONF" testparm -s "$CONF" >/dev/null 2>&1 || { sed -i "/^$B/,/^$E/d" "$CONF"; err "smb.conf после правки не проходит проверку (testparm) — правка отменена"; } systemctl enable smbd >/dev/null 2>&1; systemctl restart smbd >/dev/null 2>&1 || err "служба smbd не перезапустилась" ip=$(hostname -I | awk '{print $1}'); where="" for n in $(sed -n "/^$B/,/^$E/p" "$CONF" | sed -n 's/^\[\(.*\)\]$/\1/p'); do t="боевой"; case "$n" in *_test) t="тестовый TEST3" ;; esac where="$where${where:+, }\\\\$ip\\$n ($t)" done if [ "$dmode" = domain ]; then ok "${note}папки шлюза открыты по сети для сотрудников домена с доступом к bj: $where" else ok "${note}папки шлюза открыты по сети для $su: $where"; fi ;; *) err "неизвестная команда: $cmd" ;; esac