Первичная настройка, часть 3: установка ИШ из bj (скачать с nsd.ru или из файла; без графики: keyring, xvfb, dbus; база, служба) — проверено чистой установкой на Ubuntu 22.04; два контура НРД: каналы TEST3 и боевой в ИШ (адреса и отпечатки УЦ из документа НРД об ИШ 107), свои ключи (мастер ключа — выбор контура, профиль только к каналу контура), переключение bj между контурами с базой DEPO контура и подтверждением для боевого; папка входящих и слежение за связью — по активному контуру; пакеты ИШ в установщике и образе
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
@@ -258,7 +258,9 @@ func memTotal() uint64 {
|
||||
}
|
||||
|
||||
func (in *installer) stepPackages() (string, error) {
|
||||
pkgs := []string{"ca-certificates", "curl", "postgresql", "postgresql-client", "default-jre-headless", "pcscd", "python3", "smbclient"}
|
||||
pkgs := []string{"ca-certificates", "curl", "postgresql", "postgresql-client", "default-jre-headless", "pcscd", "python3", "smbclient",
|
||||
// для интеграционного шлюза НРД без графики (ИШ 107: пароли в gnome-keyring, движок под виртуальным дисплеем)
|
||||
"gnome-keyring", "dbus", "xvfb", "xauth", "libsecret-tools", "unzip"}
|
||||
env := append(os.Environ(), "DEBIAN_FRONTEND=noninteractive")
|
||||
// Образ кладёт пакеты со всеми зависимостями в /var/lib/bj/debs — ставим с диска, без сети.
|
||||
// Не вышло (другая система, не хватает зависимости) — обычная установка из репозитория.
|
||||
|
||||
@@ -19,7 +19,7 @@ OUT=${2:?выходной каталог}
|
||||
[ "$(id -u)" = 0 ] || { echo "нужен root"; exit 1; }
|
||||
for c in mksquashfs xorriso; do command -v $c >/dev/null || { echo "нужен $c"; exit 1; }; done
|
||||
# пакеты — как в bj-installer (stepPackages)
|
||||
BJ_PKGS="ca-certificates curl postgresql postgresql-client default-jre-headless pcscd python3 smbclient"
|
||||
BJ_PKGS="ca-certificates curl postgresql postgresql-client default-jre-headless pcscd python3 smbclient gnome-keyring dbus xvfb xauth libsecret-tools unzip"
|
||||
LAYER=ubuntu-server-minimal.ubuntu-server.bj.squashfs
|
||||
|
||||
mkdir -p "$OUT"
|
||||
|
||||
@@ -59,7 +59,7 @@ d-i partman-md/device_remove_md boolean true
|
||||
|
||||
# Пакеты: без графики; SSH и всё, что нужно bj, ставится сразу
|
||||
tasksel tasksel/first multiselect standard, ssh-server
|
||||
d-i pkgsel/include string ca-certificates curl postgresql postgresql-client default-jre-headless pcscd python3 smbclient sudo
|
||||
d-i pkgsel/include string ca-certificates curl postgresql postgresql-client default-jre-headless pcscd python3 smbclient gnome-keyring dbus xvfb xauth libsecret-tools unzip sudo
|
||||
d-i pkgsel/upgrade select safe-upgrade
|
||||
popularity-contest popularity-contest/participate boolean false
|
||||
|
||||
|
||||
Executable
+301
@@ -0,0 +1,301 @@
|
||||
#!/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
|
||||
# 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"
|
||||
}
|
||||
|
||||
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="[]"
|
||||
if runuser -u postgres -- psql -lqt 2>/dev/null | cut -d'|' -f1 | grep -qw "$DB"; then
|
||||
chans=$(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 "[]")
|
||||
fi
|
||||
echo "{\"ok\":true,\"installed\":$inst,\"version\":$(q "$(igate_version)"),\"service\":$(q "$svc"),\"engine\":$(q "$st"),\"channels\":${chans:-[]}}" ;;
|
||||
|
||||
install)
|
||||
pgrep -f "bj-igate-admin install-now" >/dev/null && err "установка ИШ уже идёт"
|
||||
file=$(j file); ver=$(j version)
|
||||
[ -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":"start","msg":"установка запущена"}' > "$STATUS"; chown bj:bj "$STATUS" 2>/dev/null
|
||||
systemd-run --unit="bj-igate-install-$(date +%s)" --collect /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 package "дистрибутив ИШ"
|
||||
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
|
||||
|
||||
st package "установка пакета ИШ"
|
||||
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
|
||||
BK=/var/backups/ish-$(date +%Y%m%d-%H%M%S); mkdir -p "$BK"
|
||||
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
|
||||
echo "$BK" > /var/backups/ish-last-backup.txt; echo "резервная копия прежнего ИШ: $BK"
|
||||
fi
|
||||
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-локи — иначе перезапуски срываются
|
||||
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
|
||||
run_as_igate_keyring "timeout 90 $IGATE_DIR/igate-cli/igate-cli --data $HOME_DIR </dev/null >/dev/null 2>&1; true"
|
||||
psqlq -c "select to_regclass('public.parameters') is not null" | grep -q t || die "ИШ не создал схему базы (igate-cli). Журнал: $LOG"
|
||||
fi
|
||||
# глобальные параметры: Web API на localhost:8090 без авторизации и запуск движка без графики
|
||||
psqlq <<'SQL' || die "не записаны параметры ИШ"
|
||||
DELETE FROM parameters WHERE chanel_id IS NULL AND name IN ('runEngineOnStartApp','server.useServer','server.host','server.port','server.scheme',
|
||||
'server.authentication.enable','server.authentication.userName','server.authentication.password','server.certificate.storage',
|
||||
'server.certificate.store.location','server.certificate.store.name','server.certificate.file.path','server.certificate.file.password',
|
||||
'wsl.httpsMode','wsl.maxConnsPerServer','wsl.proxy.mode','wsl.proxy.address','wsl.proxy.port','wsl.proxy.username','wsl.proxy.password',
|
||||
'enableDbLogging','cleanAutomatically','cleanAtTime','cleanWhenLarger','cleanVacuum','storePeriod','archiveAutomatically');
|
||||
INSERT INTO parameters(name, value, chanel_id) VALUES
|
||||
('runEngineOnStartApp','True',NULL),('server.useServer','True',NULL),('server.host','localhost',NULL),('server.port','8090',NULL),
|
||||
('server.scheme','Http',NULL),('server.authentication.enable','False',NULL),('server.authentication.userName','',NULL),
|
||||
('server.authentication.password','',NULL),('server.certificate.storage','File',NULL),('server.certificate.store.location','CurrentUser',NULL),
|
||||
('server.certificate.store.name','My',NULL),('server.certificate.file.path','',NULL),('server.certificate.file.password','',NULL),
|
||||
('wsl.httpsMode','Auto',NULL),('wsl.maxConnsPerServer','4',NULL),('wsl.proxy.mode','None',NULL),('wsl.proxy.address','',NULL),
|
||||
('wsl.proxy.port','0',NULL),('wsl.proxy.username','',NULL),('wsl.proxy.password','',NULL),('enableDbLogging','False',NULL),
|
||||
('cleanAutomatically','True',NULL),('cleanAtTime','00:30:00',NULL),('cleanWhenLarger','1024',NULL),('cleanVacuum','False',NULL),
|
||||
('storePeriod','30',NULL),('archiveAutomatically','False',NULL);
|
||||
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"
|
||||
st done "ИШ $(igate_version) установлен и работает; дальше — канал контура (TEST3 или боевой)" ;;
|
||||
|
||||
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 "ИШ не установлен"
|
||||
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',''),
|
||||
('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',:'dir' || '/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',''),('fetchThreadCount',''),('forceCryPackageEncryption',''),('inPkgReportDirectory',''),
|
||||
('inPkgReportOffloadInterval',''),('maxPackagesPerJob',''),('nsdCertificateSerialNumbers',''),('pkiDecryptMode',''),('pkiEncryptMode',''),
|
||||
('pkiSignMode',''),('pkiVerifyMode',''),('receiveProcThreadCount',''),('sendProcThreadCount',''),('updateTransitMemberListTime',''),
|
||||
('wslTlsCACertFingerprint',:'fp'),('wslTlsCertDN','*.nsd.ru'),('wslTlsCertChainLen','2'),('isM2M','True');
|
||||
-- существующие значения канала не трогаем, кроме контурных (адрес, отпечаток, криптография, депозитарный код)
|
||||
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 ;;
|
||||
|
||||
*) err "неизвестная команда: $cmd" ;;
|
||||
esac
|
||||
@@ -24,8 +24,11 @@ if [ "$ACTION" = "restart-ish" ]; then
|
||||
fi
|
||||
|
||||
# ---- сертификаты НРД в справочнике активного профиля ИШ ----
|
||||
# BJ_CHANNEL — начало кода канала ИШ контура (TEST3 | PROD): у тестового и боевого контуров свои ключи и профили
|
||||
CHAN_PREFIX=$(printf '%s' "${BJ_CHANNEL:-}" | tr -cd 'A-Z0-9')
|
||||
chan_where(){ if [ -n "$CHAN_PREFIX" ]; then echo "chanel_id in (select id from channels where code like '${CHAN_PREFIX}%')"; else echo "chanel_id is not null"; fi; }
|
||||
prof_dir(){
|
||||
PROFILE_NAME=$(su -s /bin/sh postgres -c "psql -d igate -tAc \"select value from parameters where name='cryptography.profile'\"" 2>/dev/null | tr -d '[:space:]')
|
||||
PROFILE_NAME=$(su -s /bin/sh postgres -c "psql -d igate -tAc \"select value from parameters where name='cryptography.profile' and value<>'' and $(chan_where) order by chanel_id limit 1\"" 2>/dev/null | tr -d '[:space:]')
|
||||
[ -n "$PROFILE_NAME" ] || err "профиль ИШ не задан (cryptography.profile)"
|
||||
PROFDIR="$IGATE_PROFILES/$PROFILE_NAME"
|
||||
[ -f "$PROFDIR/local.gdbm" ] && [ -f "$PROFDIR/local.pse" ] || err "нет справочника в $PROFDIR"
|
||||
@@ -197,7 +200,8 @@ PY
|
||||
fi
|
||||
# подключить профиль к ИШ: cryptography.profile в БД igate (канал 33)
|
||||
if [ "$IGATE_RES" = "ok" ]; then
|
||||
if su -s /bin/sh postgres -c "psql -d igate -tAc \"update parameters set value='$PROFILE' where name='cryptography.profile'\"" >/dev/null 2>&1; then
|
||||
# только канал контура этого ключа (BJ_CHANNEL); старый bj без контура — все каналы, как раньше. Канал с ключом включаем.
|
||||
if su -s /bin/sh postgres -c "psql -d igate -tAc \"update parameters set value='$PROFILE' where name='cryptography.profile' and $(chan_where); update channels set enable=true where id in (select chanel_id from parameters where name='cryptography.profile' and value='$PROFILE'); update parameters set value='True' where name='enable' and chanel_id in (select chanel_id from parameters where name='cryptography.profile' and value='$PROFILE')\"" >/dev/null 2>&1; then
|
||||
IGATE_RES="connected"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -3,14 +3,16 @@
|
||||
REQDIR=/var/lib/bj/keymedia-requests
|
||||
getf(){ python3 -c "import json;print(json.load(open('$1')).get('$2',''))" 2>/dev/null; }
|
||||
process_one(){
|
||||
local req="$1" id res action staging profile tdev tmnt extra out
|
||||
local req="$1" id res action staging profile tdev tmnt extra chan out
|
||||
id=$(basename "$req" .req); res="$REQDIR/$id.res"
|
||||
action=$(getf "$req" action); staging=$(getf "$req" staging); profile=$(getf "$req" profile)
|
||||
tdev=$(getf "$req" target_dev); tmnt=$(getf "$req" target_mnt); extra=$(getf "$req" extra)
|
||||
tdev=$(getf "$req" target_dev); tmnt=$(getf "$req" target_mnt); extra=$(getf "$req" extra); chan=$(getf "$req" channel)
|
||||
rm -f "$req"
|
||||
case "$action" in
|
||||
quikexport-*) out=$(BJ_EXTRA="$extra" /usr/local/sbin/bj-quikexport-admin "${action#quikexport-}" "$staging" 2>&1) ;;
|
||||
component-install) out=$(/usr/local/sbin/bj-component-install "$staging" 2>&1) ;;
|
||||
# интеграционный шлюз НРД: состояние, установка, канал контура (TEST3 | боевой); параметры — JSON в extra
|
||||
igate-status|igate-install|igate-contour) out=$(BJ_EXTRA="$extra" /usr/local/sbin/bj-igate-admin "${action#igate-}" 2>&1) ;;
|
||||
# резервное копирование и восстановление (настройки bj → bj-backup-admin); параметры — JSON в extra
|
||||
backup-config|backup-test|backup-run|backup-list|backup-verify|backup-restore) out=$(BJ_EXTRA="$extra" /usr/local/sbin/bj-backup-admin "${action#backup-}" 2>&1) ;;
|
||||
# Перезапуск службы модуля из админки. Список закрытый: чужую службу
|
||||
@@ -36,7 +38,7 @@ process_one(){
|
||||
else err=$(printf '%s' "$err" | tr -d '"\\' | tr '\n' ' '); out="{\"ok\":false,\"error\":\"$extra: $err\"}"; fi ;;
|
||||
*) out="{\"ok\":false,\"error\":\"недопустимая служба\"}" ;;
|
||||
esac ;;
|
||||
*) out=$(BJ_TARGET_DEV="$tdev" BJ_TARGET_MNT="$tmnt" BJ_EXTRA="$extra" /usr/local/sbin/bj-keymedia-install "$action" "$staging" "$profile" 2>&1) ;;
|
||||
*) out=$(BJ_TARGET_DEV="$tdev" BJ_TARGET_MNT="$tmnt" BJ_EXTRA="$extra" BJ_CHANNEL="$chan" /usr/local/sbin/bj-keymedia-install "$action" "$staging" "$profile" 2>&1) ;;
|
||||
esac
|
||||
printf '%s' "$out" > "$res.tmp"; chown bj:bj "$res.tmp" 2>/dev/null
|
||||
mv -f "$res.tmp" "$res"
|
||||
|
||||
@@ -60,6 +60,7 @@ var templateFuncs = template.FuncMap{
|
||||
"backupWhen": backupWhen,
|
||||
"depoAccPrefix": depoAccPrefix,
|
||||
"certDaysLeft": certDaysLeft,
|
||||
"currentContour": func() string { return contourOf(currentNSDProfile()) },
|
||||
"intList": func(v ...int) []int { return v },
|
||||
"nsdSegTip": nsdSegTip,
|
||||
"devOn": authTemplateFuncs["devOn"],
|
||||
|
||||
@@ -4,10 +4,14 @@ package lkgateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -47,8 +51,233 @@ func igateState(ctx context.Context, base string) (bool, string) {
|
||||
}
|
||||
|
||||
func registerIgate(mux *http.ServeMux, rc *RuntimeConfig) {
|
||||
registerIgateAdmin(mux, rc)
|
||||
mux.HandleFunc("/admin/setup/igate/state", func(w http.ResponseWriter, r *http.Request) {
|
||||
ok, msg := igateState(r.Context(), rc.Snapshot().NSD.IGWBaseURL)
|
||||
jsonOut(w, map[string]any{"ok": ok, "msg": msg, "installed": igateInstalled()})
|
||||
})
|
||||
}
|
||||
|
||||
// ---- контуры НРД: TEST3 и боевой ----
|
||||
|
||||
func currentNSDProfile() string {
|
||||
if globalRC == nil {
|
||||
return ""
|
||||
}
|
||||
return globalRC.Snapshot().NSD.Profile
|
||||
}
|
||||
|
||||
// contourOf — контур по профилю bj: prod | guest | test3.
|
||||
func contourOf(profile string) string {
|
||||
switch {
|
||||
case strings.HasPrefix(profile, "prod"):
|
||||
return "prod"
|
||||
case strings.HasPrefix(profile, "guest"):
|
||||
return "guest"
|
||||
}
|
||||
return "test3"
|
||||
}
|
||||
|
||||
// contourChannel — начало кода канала ИШ контура (канал = <код><депозитарный код>).
|
||||
func contourChannel(contour string) string {
|
||||
switch contour {
|
||||
case "prod":
|
||||
return "PROD"
|
||||
case "guest":
|
||||
return "GUEST"
|
||||
}
|
||||
return "TEST3"
|
||||
}
|
||||
|
||||
// igateExchangeDir — папка обмена канала ИШ контура (у боевого — своя, чтобы входящие контуров не смешивались).
|
||||
func igateExchangeDir(profile string) string {
|
||||
if contourOf(profile) == "prod" {
|
||||
return "/var/lib/igate/exchange-prod"
|
||||
}
|
||||
return "/var/lib/igate/exchange"
|
||||
}
|
||||
|
||||
const igateUploadDir = "/var/lib/bj/igate-upload"
|
||||
|
||||
var igateVersionRe = regexp.MustCompile(`^\d+\.\d+\.\d+$`)
|
||||
|
||||
func registerIgateAdmin(mux *http.ServeMux, rc *RuntimeConfig) {
|
||||
// состояние ИШ и каналов + что выбрано в bj
|
||||
mux.HandleFunc("/admin/setup/igate/status", func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
res, err := runKeymediaOp(ctx, "igate-status", nil)
|
||||
if err != nil {
|
||||
jsonOut(w, map[string]any{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
s := rc.Snapshot()
|
||||
res["active_contour"] = contourOf(s.NSD.Profile)
|
||||
res["active_profile"] = s.NSD.Profile
|
||||
res["deponent"] = pickStr(s.NSD.DeponentCode, orgCode(s))
|
||||
res["depo_db"] = s.Fancy.DepoDB()
|
||||
res["depo_db_test"], res["depo_db_prod"] = s.NSD.DepoDBTest, s.NSD.DepoDBProd
|
||||
jsonOut(w, res)
|
||||
})
|
||||
// установка или обновление ИШ: загруженный архив НРД или версия для скачивания с nsd.ru
|
||||
mux.HandleFunc("/admin/setup/igate/install", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
params := map[string]string{}
|
||||
if f, hdr, err := r.FormFile("file"); err == nil {
|
||||
defer f.Close()
|
||||
name := strings.ToLower(hdr.Filename)
|
||||
if !strings.HasSuffix(name, ".zip") && !strings.HasSuffix(name, ".deb") {
|
||||
jsonOut(w, map[string]any{"ok": false, "error": "нужен дистрибутив ИШ от НРД: igate-…-amd64.zip (или .deb из него)"})
|
||||
return
|
||||
}
|
||||
_ = os.MkdirAll(igateUploadDir, 0o750)
|
||||
dst := filepath.Join(igateUploadDir, "igate"+filepath.Ext(name))
|
||||
out, err := os.Create(dst)
|
||||
if err != nil {
|
||||
jsonOut(w, map[string]any{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
_, err = io.Copy(out, io.LimitReader(f, 600<<20))
|
||||
out.Close()
|
||||
if err != nil {
|
||||
jsonOut(w, map[string]any{"ok": false, "error": "загрузка: " + err.Error()})
|
||||
return
|
||||
}
|
||||
params["file"] = dst
|
||||
} else if v := strings.TrimSpace(r.FormValue("version")); igateVersionRe.MatchString(v) {
|
||||
params["version"] = v
|
||||
} else {
|
||||
jsonOut(w, map[string]any{"ok": false, "error": "загрузите архив ИШ или укажите версию вида 107.0.930"})
|
||||
return
|
||||
}
|
||||
extra, _ := json.Marshal(params)
|
||||
ctx, cancel := context.WithTimeout(r.Context(), time.Minute)
|
||||
defer cancel()
|
||||
jsonOut(w, opResult(runKeymediaOp(ctx, "igate-install", map[string]string{"extra": string(extra)})))
|
||||
})
|
||||
mux.HandleFunc("/admin/setup/igate/install-status", func(w http.ResponseWriter, r *http.Request) {
|
||||
raw, err := os.ReadFile("/var/lib/bj/igate-install-status.json")
|
||||
if err != nil {
|
||||
jsonOut(w, map[string]any{})
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
_, _ = w.Write(raw)
|
||||
})
|
||||
// канал контура в ИШ (адрес, отпечаток УЦ, папка обмена); ключ — мастером установки ключа
|
||||
mux.HandleFunc("/admin/setup/igate/contour", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
s := rc.Snapshot()
|
||||
contour, crypto := r.FormValue("contour"), r.FormValue("crypto")
|
||||
if contour != "prod" {
|
||||
contour = "test3"
|
||||
}
|
||||
if crypto != "rsa" {
|
||||
crypto = "gost"
|
||||
}
|
||||
dep := pickStr(s.NSD.DeponentCode, orgCode(s))
|
||||
if dep == "" {
|
||||
jsonOut(w, map[string]any{"ok": false, "error": "сначала укажите депозитарный код организации (шаг 4)"})
|
||||
return
|
||||
}
|
||||
extra, _ := json.Marshal(map[string]string{"contour": contour, "crypto": crypto, "deponent": dep})
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Minute)
|
||||
defer cancel()
|
||||
jsonOut(w, opResult(runKeymediaOp(ctx, "igate-contour", map[string]string{"extra": string(extra)})))
|
||||
})
|
||||
// переключить bj на контур: профиль отправки, база DEPO контура; bj перезапускается
|
||||
mux.HandleFunc("/admin/setup/igate/switch", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
contour, crypto := r.FormValue("contour"), r.FormValue("crypto")
|
||||
if contour != "prod" {
|
||||
contour = "test3"
|
||||
}
|
||||
if crypto != "rsa" {
|
||||
crypto = "gost"
|
||||
}
|
||||
s := rc.Snapshot()
|
||||
dep := pickStr(s.NSD.DeponentCode, orgCode(s))
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
// канал контура должен быть в ИШ и включён (ключ контура установлен)
|
||||
st, err := runKeymediaOp(ctx, "igate-status", nil)
|
||||
if err != nil {
|
||||
jsonOut(w, map[string]any{"ok": false, "error": "состояние ИШ: " + err.Error()})
|
||||
return
|
||||
}
|
||||
want := contourChannel(contour) + dep
|
||||
ready := false
|
||||
if chans, ok := st["channels"].([]any); ok {
|
||||
for _, c := range chans {
|
||||
if m, ok := c.(map[string]any); ok && m["code"] == want && m["enabled"] == true {
|
||||
ready = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !ready {
|
||||
jsonOut(w, map[string]any{"ok": false, "error": "в ИШ нет включённого канала " + want + ": настройте канал контура и установите ключ этого контура"})
|
||||
return
|
||||
}
|
||||
if contour == "prod" && r.FormValue("confirm") != "БОЕВОЙ" {
|
||||
jsonOut(w, map[string]any{"ok": false, "error": "для перехода на боевой контур введите слово БОЕВОЙ"})
|
||||
return
|
||||
}
|
||||
depoDB := strings.TrimSpace(r.FormValue("depo_db"))
|
||||
if depoDB != "" && !validDBName(depoDB) {
|
||||
jsonOut(w, map[string]any{"ok": false, "error": "имя базы DEPO — латинские буквы, цифры и _"})
|
||||
return
|
||||
}
|
||||
n, f := s.NSD, s.Fancy
|
||||
from := contourOf(n.Profile)
|
||||
// запоминаем базу DEPO контура, с которого уходим, и берём базу контура, на который переходим
|
||||
if from == "prod" {
|
||||
n.DepoDBProd = f.DepoDB()
|
||||
} else {
|
||||
n.DepoDBTest = f.DepoDB()
|
||||
}
|
||||
target := depoDB
|
||||
if target == "" {
|
||||
if contour == "prod" {
|
||||
target = n.DepoDBProd
|
||||
} else {
|
||||
target = n.DepoDBTest
|
||||
}
|
||||
}
|
||||
n.Profile = contour + "-" + crypto
|
||||
n.IGWBaseURL = pickStr(n.IGWBaseURL, igateDefaultURL)
|
||||
n.SwitchedAt = time.Now()
|
||||
if target != "" && target != f.DepoDB() {
|
||||
f.DepoDatabase = target
|
||||
f.DepoTablesCheck, f.DepoTablesCheckOK = "", false // таблицы и обработку в другой базе — проверить заново
|
||||
f.DepoJobCheck, f.DepoJobCheckOK = "", false
|
||||
f.DepoProcCheck, f.DepoProcCheckOK = "", false
|
||||
if err := rc.UpdateFancy(f); err != nil {
|
||||
jsonOut(w, map[string]any{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := rc.UpdateNSD(n); err != nil {
|
||||
jsonOut(w, map[string]any{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
log.Printf("nsd: контур переключён %s → %s (профиль %s, база DEPO %s)", from, contour, n.Profile, rc.Snapshot().Fancy.DepoDB())
|
||||
go func() {
|
||||
time.Sleep(2 * time.Second)
|
||||
if _, err := restartUnit(context.Background(), "bj-server"); err != nil {
|
||||
log.Printf("nsd: перезапуск bj-server после смены контура: %v", err)
|
||||
}
|
||||
}()
|
||||
name := map[string]string{"prod": "боевой", "test3": "тестовый TEST3"}[contour]
|
||||
jsonOut(w, map[string]any{"ok": true, "output": "bj переключён на " + name + " контур (база DEPO " + rc.Snapshot().Fancy.DepoDB() + ") и перезапускается — обновите страницу через 15 секунд"})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ func findMF010Files(guid string) []string {
|
||||
guidLower := strings.ToLower(strings.TrimSpace(guid))
|
||||
var xmls, dirs []string
|
||||
seen := map[string]bool{}
|
||||
_ = filepath.Walk(igwInboxDir, func(p string, info os.FileInfo, err error) error {
|
||||
_ = filepath.Walk(igwInboxDir(), func(p string, info os.FileInfo, err error) error {
|
||||
if err != nil || info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
@@ -50,7 +50,8 @@ func findMF010Files(guid string) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
const igwInboxDir = "/var/lib/igate/exchange/INBOX"
|
||||
// igwInboxDir() — папка входящих ИШ активного контура: у боевого канала своя папка обмена (exchange-prod).
|
||||
func igwInboxDir() string { return filepath.Join(igateExchangeDir(currentNSDProfile()), "INBOX") }
|
||||
|
||||
// downloadIncomingDraft отдаёт ZIP с черновиком(ами) поручения 16/1 (MF010) по
|
||||
// переводу — для загрузки в депозитарную учётную систему.
|
||||
|
||||
@@ -270,7 +270,7 @@ func collectOrderResponseRaw(contrDID string) []respFile {
|
||||
if contrDID == "" {
|
||||
return nil
|
||||
}
|
||||
entries, err := os.ReadDir(igwInboxDir)
|
||||
entries, err := os.ReadDir(igwInboxDir())
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -283,16 +283,16 @@ func collectOrderResponseRaw(contrDID string) []respFile {
|
||||
if strings.HasPrefix(dir, "#") || dir == "INBOX" { // наши M2M-пакеты — не ответ
|
||||
continue
|
||||
}
|
||||
body, _ := readPkgText(filepath.Join(igwInboxDir, dir))
|
||||
body, _ := readPkgText(filepath.Join(igwInboxDir(), dir))
|
||||
if body == "" || !strings.Contains(body, contrDID) {
|
||||
continue
|
||||
}
|
||||
files, _ := os.ReadDir(filepath.Join(igwInboxDir, dir))
|
||||
files, _ := os.ReadDir(filepath.Join(igwInboxDir(), dir))
|
||||
for _, f := range files {
|
||||
if f.IsDir() {
|
||||
continue
|
||||
}
|
||||
p := filepath.Join(igwInboxDir, dir, f.Name())
|
||||
p := filepath.Join(igwInboxDir(), dir, f.Name())
|
||||
low := strings.ToLower(f.Name())
|
||||
switch {
|
||||
case strings.HasSuffix(low, ".xml"), strings.HasSuffix(low, ".rpt"),
|
||||
@@ -340,7 +340,7 @@ func pkgIndex() []pkgIndexEntry {
|
||||
if pkgIndexCache.list != nil && time.Since(pkgIndexCache.at) < 3*time.Second {
|
||||
return pkgIndexCache.list
|
||||
}
|
||||
entries, err := os.ReadDir(igwInboxDir)
|
||||
entries, err := os.ReadDir(igwInboxDir())
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -353,7 +353,7 @@ func pkgIndex() []pkgIndexEntry {
|
||||
if strings.HasPrefix(dir, "#") || dir == "INBOX" { // наши M2M-пакеты — не ответ
|
||||
continue
|
||||
}
|
||||
body, mt := readPkgText(filepath.Join(igwInboxDir, dir))
|
||||
body, mt := readPkgText(filepath.Join(igwInboxDir(), dir))
|
||||
if body == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -325,7 +325,13 @@ func (h *setupHandlers) keyWizardInstall(w http.ResponseWriter, r *http.Request)
|
||||
defer cancel()
|
||||
|
||||
// Шаг 2-3: запись ключа на флешку + справочник + CRL (привилегированный воркер).
|
||||
res, err := runKeymediaHelper(ctx, "install", staging, profileName, targetDev, targetMnt)
|
||||
// контур ключа: профиль Валидаты привязывается только к каналу ИШ этого контура (у TEST3 и боевого — свои ключи)
|
||||
contour := r.FormValue("contour")
|
||||
if contour != "prod" {
|
||||
contour = "test3"
|
||||
}
|
||||
res, err := runKeymediaOp(ctx, "install", map[string]string{"staging": staging, "profile": profileName,
|
||||
"target_dev": targetDev, "target_mnt": targetMnt, "channel": contourChannel(contour)})
|
||||
h.keyWiz.mu.Lock()
|
||||
if err != nil || res["ok"] != true {
|
||||
msg := errStr(err)
|
||||
|
||||
@@ -256,7 +256,7 @@ func nsdPrettyDate(s string) string {
|
||||
}
|
||||
|
||||
func workerCertsList(ctx context.Context) (string, []NSDStoreCert, error) {
|
||||
res, err := runKeymediaOp(ctx, "certs-list", nil)
|
||||
res, err := runKeymediaOp(ctx, "certs-list", map[string]string{"channel": contourChannel(contourOf(currentNSDProfile()))})
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
@@ -434,7 +434,7 @@ func nsdCertsApply(ctx context.Context, rc *RuntimeConfig, trigger string) (stri
|
||||
return "", false, fmt.Errorf("staging: %w", err)
|
||||
}
|
||||
}
|
||||
res, err := runKeymediaOp(ctx, "certs-apply", map[string]string{"staging": dir, "extra": strings.Join(removes, ",")})
|
||||
res, err := runKeymediaOp(ctx, "certs-apply", map[string]string{"staging": dir, "extra": strings.Join(removes, ","), "channel": contourChannel(contourOf(currentNSDProfile()))})
|
||||
if err != nil {
|
||||
msg := "НЕ ПРИМЕНЕНО (" + trigger + "): " + err.Error() + "\nплан: " + strings.Join(what, "; ")
|
||||
nsdSaveState(rc, profile, rows, store, msg, false)
|
||||
|
||||
@@ -213,7 +213,7 @@ func pkgFiles(dir string) []respFile {
|
||||
|
||||
// nsdOrderDocs — документы НРД по поручениям (contr_d_id), по времени.
|
||||
func nsdOrderDocs(contrDIDs ...string) []nsdDoc {
|
||||
entries, err := os.ReadDir(igwInboxDir)
|
||||
entries, err := os.ReadDir(igwInboxDir())
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -229,7 +229,7 @@ func nsdOrderDocs(contrDIDs ...string) []nsdDoc {
|
||||
if !e.IsDir() || strings.HasPrefix(dir, "#") || dir == "INBOX" || seen[dir] {
|
||||
continue
|
||||
}
|
||||
full := filepath.Join(igwInboxDir, dir)
|
||||
full := filepath.Join(igwInboxDir(), dir)
|
||||
body, mt := readPkgText(full)
|
||||
if body == "" || !strings.Contains(body, cd) {
|
||||
continue
|
||||
|
||||
@@ -38,9 +38,15 @@ type nsdPoll struct {
|
||||
// nsdLastPolls — итоги опросов НРД из хвоста журнала ИШ: последний опрос и первый неудачный после
|
||||
// последнего успешного (начало текущего сбоя; nil — сбоя нет).
|
||||
func nsdLastPolls(tail []byte) (last, firstFail *nsdPoll) {
|
||||
return nsdLastPollsFor(tail, "")
|
||||
}
|
||||
|
||||
// nsdLastPollsFor — то же для канала одного контура: при двух каналах в ИШ (TEST3 и боевой) опросы чужого
|
||||
// контура не учитываются. prefix — начало имени канала в журнале («TEST3», «PROD»); пусто — все.
|
||||
func nsdLastPollsFor(tail []byte, prefix string) (last, firstFail *nsdPoll) {
|
||||
for _, ln := range strings.Split(string(tail), "\n") {
|
||||
m := nsdPollRe.FindStringSubmatch(strings.TrimRight(ln, "\r"))
|
||||
if m == nil {
|
||||
if m == nil || (prefix != "" && !strings.HasPrefix(strings.ToUpper(strings.TrimSpace(m[2])), prefix)) {
|
||||
continue
|
||||
}
|
||||
at, err := time.ParseInLocation("2006-01-02 15:04:05", m[1], time.Local)
|
||||
@@ -105,7 +111,7 @@ func nsdLinkTick(rc *RuntimeConfig, now time.Time) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
last, firstFail := nsdLastPolls(tail)
|
||||
last, firstFail := nsdLastPollsFor(tail, contourChannel(contourOf(rc.Snapshot().NSD.Profile)))
|
||||
if last == nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -199,7 +199,7 @@ func scanForm16Drafts(store *OutgoingStore) []*OutgoingDeal {
|
||||
// по сделкам в работе, а не по всем, что когда-либо были.
|
||||
func scanForm16DraftsOpt(store *OutgoingStore, withResponses bool) []*OutgoingDeal {
|
||||
byGUID := map[string]*OutgoingDeal{}
|
||||
_ = filepath.Walk(igwInboxDir, func(p string, info os.FileInfo, err error) error {
|
||||
_ = filepath.Walk(igwInboxDir(), func(p string, info os.FileInfo, err error) error {
|
||||
if err != nil || info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -255,6 +255,12 @@ type NSDSettings struct {
|
||||
// версиях младше — НЕ допускается. По умолчанию false (совместимо со старым
|
||||
// ИШ — элемент не добавляется).
|
||||
IGWv904 bool `json:"igw_v904"`
|
||||
|
||||
// Контуры (05.10.2026): база DEPO для TEST3 и для боевого — на тесте часто копия учёта. Запоминаются при
|
||||
// переключении контура, чтобы вернуться без перенастройки.
|
||||
DepoDBTest string `json:"depo_db_test,omitempty"`
|
||||
DepoDBProd string `json:"depo_db_prod,omitempty"`
|
||||
SwitchedAt time.Time `json:"switched_at,omitempty"`
|
||||
}
|
||||
|
||||
// LKSettings — настройки callback в ЛК клиента.
|
||||
|
||||
@@ -715,6 +715,7 @@ func (h *setupHandlers) saveNSD(w http.ResponseWriter, r *http.Request) {
|
||||
IGWv904: r.FormValue("igw_v904") == "on",
|
||||
}
|
||||
cur := h.rc.Snapshot().NSD
|
||||
s.DepoDBTest, s.DepoDBProd, s.SwitchedAt = cur.DepoDBTest, cur.DepoDBProd, cur.SwitchedAt
|
||||
if r.FormValue("keep_requisites") == "1" { // мастер: реквизиты НРД — на шаге «Организация»
|
||||
s.DeponentCode, s.AccountID, s.SectionID = cur.DeponentCode, cur.AccountID, cur.SectionID
|
||||
}
|
||||
|
||||
@@ -74,6 +74,13 @@
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style="font-weight:600;display:block;margin-bottom:6px">Для какого контура НРД этот ключ</label>
|
||||
<label style="display:flex;gap:8px;align-items:center;font-weight:400"><input type="radio" name="contour" value="test3" style="width:auto" {{if ne currentContour "prod"}}checked{{end}}> Тестовый TEST3 <span class="muted" style="font-size:12px">— сертификат от «Тестовый УЦ»</span></label>
|
||||
<label style="display:flex;gap:8px;align-items:center;font-weight:400"><input type="radio" name="contour" value="prod" style="width:auto" {{if eq currentContour "prod"}}checked{{end}}> Боевой <span class="muted" style="font-size:12px">— выдан УЦ Московской биржи для работы</span></label>
|
||||
<span class="muted" style="font-size:12px">У тестового и боевого контуров разные ключи: ключ подключается только к каналу ИШ своего контура.</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style="font-weight:600;display:block;margin-bottom:6px">Имя профиля в справочнике (необязательно)</label>
|
||||
<input type="text" name="profile_name" placeholder="Авто из архива (напр. PrUser1046)" autocomplete="off"
|
||||
|
||||
@@ -460,40 +460,23 @@ a.wstep:hover .bub { box-shadow:0 0 0 4px var(--accent-weak); }
|
||||
<strong>Что это.</strong> Интеграционный шлюз — программа НРД на этом же сервере: через неё bj отправляет пакеты M2M в НРД и получает ответы. bj работает со шлюзом по его адресу на сервере (обычно <code>http://localhost:8090</code>).<br>
|
||||
<strong>К кому обратиться.</strong> Дистрибутив ИШ выпускает НРД (<a href="https://www.nsd.ru/workflow/system/programs/" target="_blank" rel="noopener">nsd.ru/workflow/system/programs</a>). Доступ к контурам — сначала тестовому TEST3, затем боевому — НРД открывает по заявке: обратитесь к своему менеджеру НРД или в поддержку электронного документооборота.
|
||||
</div>
|
||||
<p id="igw-state" class="muted" style="margin:8px 0">Проверяю шлюз…</p>
|
||||
{{template "igateInstall" .}}
|
||||
<form method="post" action="/admin/setup/nsd" style="margin-top:12px;display:grid;gap:10px;max-width:680px">
|
||||
<details style="margin-top:8px"><summary style="cursor:pointer;color:var(--accent);font-size:13px">Дополнительно: адрес шлюза (обычно не нужно)</summary>
|
||||
<form method="post" action="/admin/setup/nsd" style="margin-top:10px;display:grid;gap:10px;max-width:680px">
|
||||
<input type="hidden" name="return_to" value="/admin/wizard?step=8">
|
||||
{{if .Settings.NSD.IGWv904}}<input type="hidden" name="igw_v904" value="on">{{end}}
|
||||
<input type="hidden" name="keep_requisites" value="1">
|
||||
<label style="margin:0">Контур НРД
|
||||
<select name="profile" style="padding:8px;width:100%;box-sizing:border-box;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px;margin-top:4px">
|
||||
<option value="test3-gost" {{if eq .Settings.NSD.Profile "test3-gost"}}selected{{end}}>Тестовый TEST3 · ГОСТ</option>
|
||||
<option value="test3-rsa" {{if eq .Settings.NSD.Profile "test3-rsa"}}selected{{end}}>Тестовый TEST3 · RSA</option>
|
||||
{{if or devOn (eq .Settings.NSD.Profile "guest-gost") (eq .Settings.NSD.Profile "guest-rsa")}} <option value="guest-gost" {{if eq .Settings.NSD.Profile "guest-gost"}}selected{{end}}>GUEST · ГОСТ (для разработки)</option>
|
||||
<option value="guest-rsa" {{if eq .Settings.NSD.Profile "guest-rsa"}}selected{{end}}>GUEST · RSA (для разработки)</option>
|
||||
{{end}} <option value="prod-gost" {{if or (eq .Settings.NSD.Profile "prod-gost") (eq .Settings.NSD.Profile "prod")}}selected{{end}}>Боевой · ГОСТ</option>
|
||||
<option value="prod-rsa" {{if eq .Settings.NSD.Profile "prod-rsa"}}selected{{end}}>Боевой · RSA</option>
|
||||
</select></label>
|
||||
<input type="hidden" name="profile" value="{{if .Settings.NSD.Profile}}{{.Settings.NSD.Profile}}{{else}}test3-gost{{end}}">
|
||||
<label style="margin:0">Адрес шлюза на этом сервере
|
||||
<input type="text" name="igw_base_url" value="{{if .Settings.NSD.IGWBaseURL}}{{.Settings.NSD.IGWBaseURL}}{{else}}http://localhost:8090{{end}}" placeholder="http://localhost:8090" style="padding:8px;width:100%;box-sizing:border-box;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px;margin-top:4px"></label>
|
||||
<p id="igw-onyx" style="display:none;color:var(--err);font-size:13px;margin:0">Это адрес веб-сервиса НРД (ONYX), а не шлюза: bj работает через ИШ на этом сервере. Укажите адрес шлюза — обычно <a href="#" id="igw-def">http://localhost:8090</a>.</p>
|
||||
<p class="muted" style="font-size:12.5px;margin:0">По «Сохранить» bj проверит, что шлюз отвечает по этому адресу.</p>
|
||||
<script>(function(){var u=document.querySelector('[name=igw_base_url]'),w=document.getElementById('igw-onyx');
|
||||
function chk(){w.style.display=/onyx|nsd\.ru/i.test(u.value)?'block':'none'} u.addEventListener('input',chk); chk();
|
||||
document.getElementById('igw-def').onclick=function(e){e.preventDefault();u.value='http://localhost:8090';chk()};})();</script>
|
||||
<div><button type="submit" class="btn">Сохранить и проверить</button></div>
|
||||
</form>
|
||||
</details>
|
||||
<p class="muted" style="font-size:13px;margin-top:12px">Депозитарные реквизиты в НРД: {{if and .Settings.NSD.DeponentCode .Settings.NSD.AccountID .Settings.NSD.SectionID}}код <code>{{.Settings.NSD.DeponentCode}}</code>, счёт <code>{{.Settings.NSD.AccountID}}</code>, раздел <code>{{.Settings.NSD.SectionID}}</code>{{else}}<span style="color:var(--err)">не заданы</span>{{end}} — на <a href="/admin/wizard?step=4#nsd-req">шаге 4</a>.</p>
|
||||
<script>
|
||||
(function(){
|
||||
var el=document.getElementById('igw-state');
|
||||
fetch('/admin/setup/igate/state',{cache:'no-store'}).then(function(r){return r.json()}).then(function(d){
|
||||
el.className=''; el.innerHTML=d.ok?'<span style="color:var(--ok)">✓ '+d.msg+'</span>':'<span style="color:var(--err)">✗ '+d.msg+'</span>';
|
||||
document.dispatchEvent(new CustomEvent('igate-state',{detail:d}));
|
||||
}).catch(function(){ el.textContent='Состояние шлюза не получено.'; });
|
||||
})();
|
||||
</script>
|
||||
<div style="margin-top:20px;display:flex;justify-content:space-between">
|
||||
<a href="/admin/wizard?step=7" class="btn" style="background:var(--card);color:var(--text);border:1px solid var(--border-strong,#c9cdd8);text-decoration:none">← К шагу 7</a>
|
||||
{{if .Done.NSD}}<a href="/admin/wizard?step=9" class="btn" style="text-decoration:none">К шагу 9 →</a>{{else}}<a href="/admin/wizard?step=9&skip=nsd" class="btn" style="background:var(--card);color:var(--text);border:1px solid var(--border-strong,#c9cdd8);text-decoration:none">Пропустить ({{if devOn}}mock-режим{{else}}обмен с НРД не настроен{{end}}) →</a>{{end}}
|
||||
|
||||
@@ -1 +1,115 @@
|
||||
{{define "igateInstall"}}{{end}}
|
||||
{{/* Шлюз НРД: установка ИШ, каналы контуров TEST3 и боевого, переключение bj между контурами (мастер, шаг 8). */}}
|
||||
{{define "igateInstall"}}
|
||||
<style>
|
||||
#ig-box h3{margin:0 0 6px;font-size:15px}
|
||||
#ig-box label.f{display:block;margin:0;font-size:13px}
|
||||
#ig-box label.f input,#ig-box label.f select{display:block;margin-top:4px;padding:7px 9px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px}
|
||||
#ig-ct{width:100%;margin-top:8px}
|
||||
#ig-ct td{vertical-align:top;padding:6px 8px 6px 0}
|
||||
#ig-box .btn.sm{padding:4px 10px;font-size:12.5px}
|
||||
#ig-box .btn.ghost{background:var(--card);color:var(--text);border:1px solid var(--border-strong,#c9cdd8)}
|
||||
</style>
|
||||
<div id="ig-box">
|
||||
<div class="card" style="margin:10px 0">
|
||||
<h3>Шлюз на этом сервере</h3>
|
||||
<div id="ig-head" class="muted">Проверяю…</div>
|
||||
<details id="ig-inst" style="margin-top:8px">
|
||||
<summary style="cursor:pointer;color:var(--accent)" id="ig-inst-sum">Установить или обновить ИШ</summary>
|
||||
<p class="muted" style="font-size:13px;margin:8px 0">Дистрибутив ИШ для Linux выпускает НРД. bj скачает его с сайта НРД сам или возьмёт загруженный архив <code>igate-…-amd64.zip</code>. Установка занимает 2–5 минут: пакеты для работы без графики, пользователь igate, база ИШ, служба. Настройки ИШ сохраняются, прежняя версия копируется в /var/backups.</p>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:end">
|
||||
<label class="f">Версия на сайте НРД<input id="ig-ver" value="107.0.930" style="width:140px"></label>
|
||||
<button type="button" class="btn" id="ig-dl">Скачать с сайта НРД и установить</button>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-top:10px">
|
||||
<input type="file" id="ig-file" accept=".zip,.deb">
|
||||
<button type="button" class="btn ghost" id="ig-up">Установить из файла</button>
|
||||
</div>
|
||||
<div id="ig-out" style="margin-top:8px;font-size:13px"></div>
|
||||
<pre id="ig-log" style="display:none;max-height:180px;overflow:auto;font-size:11px;margin-top:6px"></pre>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin:10px 0">
|
||||
<h3>Контуры НРД</h3>
|
||||
<p class="muted" style="font-size:13px;margin:0 0 8px">Сначала работают на тестовом контуре TEST3; когда переводы на нём проходят — переходят на боевой. У каждого контура свой ключ (в мастере установки ключа выберите контур) и свой канал в ИШ. bj работает с одним контуром — активным.</p>
|
||||
<label class="f" style="max-width:220px">Криптография<select id="ig-crypto"><option value="gost">ГОСТ</option><option value="rsa">RSA</option></select></label>
|
||||
<table id="ig-ct"><tbody></tbody></table>
|
||||
<div id="ig-prod" style="display:none;border:1px solid var(--warn);border-radius:8px;padding:10px 12px;margin-top:10px">
|
||||
<b>Переход на боевой контур</b>
|
||||
<p class="muted" style="font-size:13px;margin:6px 0">После перехода bj отправляет в НРД настоящие переводы. На боевом контуре обычно работает настоящий учёт DEPO — укажите его базу (на TEST3 часто использовали копию). Вернуться на TEST3 можно здесь же.</p>
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:end">
|
||||
<label class="f">База DEPO для боевого контура<input id="ig-depo" placeholder="как сейчас" style="width:220px"></label>
|
||||
<label class="f">Для подтверждения введите БОЕВОЙ<input id="ig-confirm" autocomplete="off" style="width:160px"></label>
|
||||
<button type="button" class="btn" id="ig-goprod" style="background:var(--err);border-color:var(--err)">Перейти на боевой</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="ig-cout" style="margin-top:8px;font-size:13px"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function(){
|
||||
var head=document.getElementById('ig-head'), tb=document.querySelector('#ig-ct tbody'), cout=document.getElementById('ig-cout'), out=document.getElementById('ig-out'), lg=document.getElementById('ig-log');
|
||||
function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,function(c){return{'&':'&','<':'<','>':'>','"':'"'}[c]})}
|
||||
function ok(t){return '<span style="color:var(--ok)">✓ '+esc(t)+'</span>'} function bad(t){return '<span style="color:var(--err)">✗ '+esc(t)+'</span>'}
|
||||
function post(u,p){return fetch(u,{method:'POST',body:p,cache:'no-store'}).then(function(r){return r.json()})}
|
||||
var st=null;
|
||||
function load(){
|
||||
fetch('/admin/setup/igate/status',{cache:'no-store'}).then(function(r){return r.json()}).then(function(d){
|
||||
st=d;
|
||||
if(!d.ok){ head.innerHTML=bad(d.error); return; }
|
||||
if(!d.installed){ head.innerHTML=bad('ИШ на этом сервере не установлен')+' — установите его ниже.'; document.getElementById('ig-inst').open=true; }
|
||||
else head.innerHTML=(d.engine==='Running'?ok('ИШ '+d.version+' работает'):bad('ИШ '+d.version+' установлен, движок: '+(d.engine||'не отвечает')+', служба: '+d.service));
|
||||
if(d.installed) document.getElementById('ig-inst-sum').textContent='Обновить ИШ (сейчас '+d.version+')';
|
||||
var dep=d.deponent||'', cr=document.getElementById('ig-crypto').value;
|
||||
var rows=[['test3','Тестовый TEST3'],['prod','Боевой']].map(function(x){
|
||||
var code=(x[0]==='prod'?'PROD':'TEST3')+dep, ch=(d.channels||[]).filter(function(c){return c.code===code})[0], act=d.active_contour===x[0];
|
||||
var s=[], a=[];
|
||||
if(!dep) s.push(bad('нет депозитарного кода (шаг 4)'));
|
||||
else if(!ch){ s.push('<span class="muted">канала в ИШ нет</span>'); if(d.installed) a.push('<button type="button" class="btn sm" data-act="contour" data-c="'+x[0]+'">Настроить канал</button>'); }
|
||||
else {
|
||||
s.push(ok('канал '+ch.code)+' <span class="muted" style="font-size:12px">'+esc(ch.crypto+' · '+ch.endpoint)+'</span>');
|
||||
s.push(ch.profile?ok('ключ: профиль '+ch.profile):bad('ключ этого контура не установлен'));
|
||||
s.push(ch.enabled?ok('канал включён'):'<span class="muted">канал выключен до установки ключа</span>');
|
||||
a.push('<button type="button" class="btn sm ghost" data-act="contour" data-c="'+x[0]+'">Обновить канал</button>');
|
||||
if(!ch.profile) a.push('<a class="btn sm" href="/admin/setup/keywizard" style="text-decoration:none">Установить ключ</a>');
|
||||
if(ch.enabled && !act) a.push(x[0]==='prod'?'<button type="button" class="btn sm" data-act="showprod">Перейти на боевой…</button>':'<button type="button" class="btn sm" data-act="switch" data-c="test3">Сделать активным</button>');
|
||||
}
|
||||
return '<tr><td style="white-space:nowrap;width:130px"><b>'+x[1]+'</b>'+(act?'<br><span style="color:var(--accent);font-size:12px">● активный</span>':'')+'</td><td>'+s.join('<br>')+(a.length?'<div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:6px">'+a.join('')+'</div>':'')+'</td></tr>';
|
||||
});
|
||||
tb.innerHTML=rows.join('');
|
||||
document.getElementById('ig-depo').placeholder=d.depo_db_prod||d.depo_db||'как сейчас';
|
||||
tb.querySelectorAll('button[data-act]').forEach(function(b){ b.onclick=function(){ act(b.dataset.act,b.dataset.c,b); }; });
|
||||
}).catch(function(){ head.textContent='Состояние ИШ не получено.'; });
|
||||
}
|
||||
function act(a,c,b){
|
||||
if(a==='showprod'){ document.getElementById('ig-prod').style.display='block'; return; }
|
||||
var p=new URLSearchParams(); p.set('contour',c); p.set('crypto',document.getElementById('ig-crypto').value);
|
||||
if(a==='contour'){ b.disabled=true; cout.textContent='Записываю канал и перезапускаю ИШ (до 2 минут)…';
|
||||
post('/admin/setup/igate/contour',p).then(function(d){ b.disabled=false; cout.innerHTML=d.ok?ok(d.output):bad(d.error); load(); }); }
|
||||
if(a==='switch'){ if(!confirm('Переключить bj на тестовый контур TEST3?')) return; cout.textContent='Переключаю…';
|
||||
post('/admin/setup/igate/switch',p).then(function(d){ cout.innerHTML=d.ok?ok(d.output):bad(d.error); if(d.ok) setTimeout(function(){location.reload()},15000); }); }
|
||||
}
|
||||
document.getElementById('ig-goprod').onclick=function(){
|
||||
var p=new URLSearchParams(); p.set('contour','prod'); p.set('crypto',document.getElementById('ig-crypto').value);
|
||||
p.set('depo_db',document.getElementById('ig-depo').value.trim()); p.set('confirm',document.getElementById('ig-confirm').value.trim());
|
||||
if(!confirm('Перейти на БОЕВОЙ контур? bj начнёт отправлять в НРД настоящие переводы.')) return;
|
||||
cout.textContent='Переключаю…';
|
||||
post('/admin/setup/igate/switch',p).then(function(d){ cout.innerHTML=d.ok?ok(d.output):bad(d.error); if(d.ok) setTimeout(function(){location.reload()},15000); });
|
||||
};
|
||||
document.getElementById('ig-crypto').onchange=load;
|
||||
function poll(){ var t0=Date.now(), iv=setInterval(function(){
|
||||
fetch('/admin/setup/igate/install-status',{cache:'no-store'}).then(function(r){return r.json()}).then(function(s){
|
||||
if(!s.stage) return; out.textContent='Установка: '+s.msg; if(s.log){ lg.style.display='block'; lg.textContent=s.log; lg.scrollTop=lg.scrollHeight; }
|
||||
if(s.stage==='done'){ clearInterval(iv); out.innerHTML=ok(s.msg); load(); }
|
||||
if(s.stage==='error'){ clearInterval(iv); out.innerHTML=bad(s.msg); }
|
||||
}).catch(function(){});
|
||||
if(Date.now()-t0>1200000) clearInterval(iv); },3000); }
|
||||
document.getElementById('ig-dl').onclick=function(){ var p=new URLSearchParams(); p.set('version',document.getElementById('ig-ver').value.trim());
|
||||
out.textContent='Запускаю…'; post('/admin/setup/igate/install',p).then(function(d){ if(!d.ok){out.innerHTML=bad(d.error);return} out.textContent=d.output; poll(); }); };
|
||||
document.getElementById('ig-up').onclick=function(){ var f=document.getElementById('ig-file').files[0]; if(!f){ out.textContent='Выберите файл igate-…-amd64.zip'; return; }
|
||||
var fd=new FormData(); fd.append('file',f); out.textContent='Загружаю '+f.name+'…';
|
||||
fetch('/admin/setup/igate/install',{method:'POST',body:fd}).then(function(r){return r.json()}).then(function(d){ if(!d.ok){out.innerHTML=bad(d.error);return} out.textContent=d.output; poll(); }); };
|
||||
load();
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
Reference in New Issue
Block a user