Files
Bridge-and-Join-s/deploy/linux/sbin/bj-igate-admin
T
fontvielle c9ab60130f ИШ: этапы обновления шлюза в окне обновления bj, проверка нового ИШ, свежая версия в строке НРД
- окно обновления bj после перезапуска ведёт обновление ИШ по этапам (загрузка, остановка,
  резервная копия, установка, запуск, проверка) и закрывается только после проверки;
  пока шлюз обновляется, то же окно на любой странице
- bj-igate-admin: этапы stop/backup/verify; после запуска сверяются версия, служба, движок,
  каналы и ключ с прежними (иначе автооткат), обмен с НРД
- версия ИШ в строке НРД читается всегда заново (кэш 6 ч — только сайт НРД);
  после установки версии пересобираются сразу

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
2026-10-21 22:50:43 +03:00

529 lines
44 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-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" <<EOF
#!/usr/bin/env bash
# bj: запуск ИШ без графики — gnome-keyring (пароли ИШ 107) + виртуальный дисплей; из ExecStart через dbus-run-session.
export HOME=$HOME_DIR XDG_DATA_HOME="$HOME_DIR/.local/share" XDG_RUNTIME_DIR="/run/user/\$(id -u)" GIO_USE_VFS=local
eval "\$(printf '%s\n' "\$(cat "\$HOME/.keyring-pass")" | gnome-keyring-daemon --replace --unlock --components=secrets,pkcs11 2>/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" <<EOF
[Unit]
Description=Интеграционный шлюз НРД (ИШ)
After=network-online.target postgresql.service
Wants=network-online.target
StartLimitIntervalSec=300
StartLimitBurst=8
[Service]
User=$SVC_USER
WorkingDirectory=$HOME_DIR
# самоочистка перед стартом: осиротевшие сокеты, keyring, X-локи — иначе перезапуски срываются
# каталог сеанса igate (XDG_RUNTIME_DIR): обычно его создаёт systemd-logind (linger), но без него keyring не стартует
ExecStartPre=+/usr/bin/install -d -o $SVC_USER -g $SVC_USER -m 700 /run/user/$uid
ExecStartPre=-/bin/bash -c 'rm -f /tmp/CoreFxPipe_NSD.IGate.* $HOME_DIR/.local/share/keyrings/login.keyring; find /tmp -maxdepth 1 -name ".X9*-lock" -delete 2>/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 >/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