diff --git a/cmd/bj-installer/main.go b/cmd/bj-installer/main.go index e3d1d1c..dd1ec50 100644 --- a/cmd/bj-installer/main.go +++ b/cmd/bj-installer/main.go @@ -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 — ставим с диска, без сети. // Не вышло (другая система, не хватает зависимости) — обычная установка из репозитория. diff --git a/deploy/iso/build-ubuntu-updates-layer.sh b/deploy/iso/build-ubuntu-updates-layer.sh index 08f9d4a..9146ffc 100755 --- a/deploy/iso/build-ubuntu-updates-layer.sh +++ b/deploy/iso/build-ubuntu-updates-layer.sh @@ -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" diff --git a/deploy/iso/preseed.cfg b/deploy/iso/preseed.cfg index 4a27dc9..0a5693c 100644 --- a/deploy/iso/preseed.cfg +++ b/deploy/iso/preseed.cfg @@ -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 diff --git a/deploy/linux/sbin/bj-igate-admin b/deploy/linux/sbin/bj-igate-admin new file mode 100755 index 0000000..0658395 --- /dev/null +++ b/deploy/linux/sbin/bj-igate-admin @@ -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" </dev/null)" +export GNOME_KEYRING_CONTROL SSH_AUTH_SOCK +cd "\$HOME" +exec xvfb-run -a $IGATE_DIR/igate-svc/igate-svc +EOF + chmod 755 "$WRAPPER" + rm -f /etc/systemd/system/igate.service.d/override.conf + cat > "$UNIT" </dev/null; true' +ExecStart=/usr/bin/dbus-run-session -- $WRAPPER +Restart=on-failure +RestartSec=8 + +[Install] +WantedBy=multi-user.target +EOF + systemctl daemon-reload + + st schema "база ИШ: создание схемы" + if ! psqlq -c "select to_regclass('public.parameters') is not null" | grep -q t; then + run_as_igate_keyring "timeout 90 $IGATE_DIR/igate-cli/igate-cli --data $HOME_DIR /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 diff --git a/deploy/linux/sbin/bj-keymedia-install b/deploy/linux/sbin/bj-keymedia-install index f1dbf98..0c31018 100755 --- a/deploy/linux/sbin/bj-keymedia-install +++ b/deploy/linux/sbin/bj-keymedia-install @@ -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 diff --git a/deploy/linux/sbin/bj-keymedia-worker b/deploy/linux/sbin/bj-keymedia-worker index d7e63d9..f1250d6 100755 --- a/deploy/linux/sbin/bj-keymedia-worker +++ b/deploy/linux/sbin/bj-keymedia-worker @@ -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" diff --git a/internal/lkgateway/admin.go b/internal/lkgateway/admin.go index 4543ce9..c32d14b 100644 --- a/internal/lkgateway/admin.go +++ b/internal/lkgateway/admin.go @@ -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"], diff --git a/internal/lkgateway/igate.go b/internal/lkgateway/igate.go index 2c750e7..d926639 100644 --- a/internal/lkgateway/igate.go +++ b/internal/lkgateway/igate.go @@ -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 секунд"}) + }) +} diff --git a/internal/lkgateway/incoming_draft.go b/internal/lkgateway/incoming_draft.go index 7b43fff..52019bf 100644 --- a/internal/lkgateway/incoming_draft.go +++ b/internal/lkgateway/incoming_draft.go @@ -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) по // переводу — для загрузки в депозитарную учётную систему. diff --git a/internal/lkgateway/incoming_order.go b/internal/lkgateway/incoming_order.go index 56e24b5..9123d11 100644 --- a/internal/lkgateway/incoming_order.go +++ b/internal/lkgateway/incoming_order.go @@ -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 } diff --git a/internal/lkgateway/keywizard.go b/internal/lkgateway/keywizard.go index 833a2ea..32c90b5 100644 --- a/internal/lkgateway/keywizard.go +++ b/internal/lkgateway/keywizard.go @@ -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) diff --git a/internal/lkgateway/nsdcerts.go b/internal/lkgateway/nsdcerts.go index a7fb49d..d8c03cc 100644 --- a/internal/lkgateway/nsdcerts.go +++ b/internal/lkgateway/nsdcerts.go @@ -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) diff --git a/internal/lkgateway/nsddocs.go b/internal/lkgateway/nsddocs.go index fc69623..2da2fc8 100644 --- a/internal/lkgateway/nsddocs.go +++ b/internal/lkgateway/nsddocs.go @@ -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 diff --git a/internal/lkgateway/nsdlinkwatch.go b/internal/lkgateway/nsdlinkwatch.go index 298513c..9ff91e6 100644 --- a/internal/lkgateway/nsdlinkwatch.go +++ b/internal/lkgateway/nsdlinkwatch.go @@ -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 } diff --git a/internal/lkgateway/outgoing_order.go b/internal/lkgateway/outgoing_order.go index 7064ca2..b5c1312 100644 --- a/internal/lkgateway/outgoing_order.go +++ b/internal/lkgateway/outgoing_order.go @@ -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 } diff --git a/internal/lkgateway/runtimeconfig.go b/internal/lkgateway/runtimeconfig.go index 8a464ff..97af9aa 100644 --- a/internal/lkgateway/runtimeconfig.go +++ b/internal/lkgateway/runtimeconfig.go @@ -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 в ЛК клиента. diff --git a/internal/lkgateway/setup.go b/internal/lkgateway/setup.go index 16a8914..0b91fe1 100644 --- a/internal/lkgateway/setup.go +++ b/internal/lkgateway/setup.go @@ -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 } diff --git a/internal/lkgateway/web/templates/admin_keywizard.html b/internal/lkgateway/web/templates/admin_keywizard.html index aedb9ef..590531b 100644 --- a/internal/lkgateway/web/templates/admin_keywizard.html +++ b/internal/lkgateway/web/templates/admin_keywizard.html @@ -74,6 +74,13 @@ {{end}} +
+ + + + У тестового и боевого контуров разные ключи: ключ подключается только к каналу ИШ своего контура. +
+
Что это. Интеграционный шлюз — программа НРД на этом же сервере: через неё bj отправляет пакеты M2M в НРД и получает ответы. bj работает со шлюзом по его адресу на сервере (обычно http://localhost:8090).
К кому обратиться. Дистрибутив ИШ выпускает НРД (nsd.ru/workflow/system/programs). Доступ к контурам — сначала тестовому TEST3, затем боевому — НРД открывает по заявке: обратитесь к своему менеджеру НРД или в поддержку электронного документооборота.
-

Проверяю шлюз…

{{template "igateInstall" .}} -
+
Дополнительно: адрес шлюза (обычно не нужно) + {{if .Settings.NSD.IGWv904}}{{end}} - + -

По «Сохранить» bj проверит, что шлюз отвечает по этому адресу.

+

Депозитарные реквизиты в НРД: {{if and .Settings.NSD.DeponentCode .Settings.NSD.AccountID .Settings.NSD.SectionID}}код {{.Settings.NSD.DeponentCode}}, счёт {{.Settings.NSD.AccountID}}, раздел {{.Settings.NSD.SectionID}}{{else}}не заданы{{end}} — на шаге 4.

-
← К шагу 7 {{if .Done.NSD}}К шагу 9 →{{else}}Пропустить ({{if devOn}}mock-режим{{else}}обмен с НРД не настроен{{end}}) →{{end}} diff --git a/internal/lkgateway/web/templates/part_igate.html b/internal/lkgateway/web/templates/part_igate.html index a9a67f0..fb5e016 100644 --- a/internal/lkgateway/web/templates/part_igate.html +++ b/internal/lkgateway/web/templates/part_igate.html @@ -1 +1,115 @@ -{{define "igateInstall"}}{{end}} +{{/* Шлюз НРД: установка ИШ, каналы контуров TEST3 и боевого, переключение bj между контурами (мастер, шаг 8). */}} +{{define "igateInstall"}} + +
+
+

Шлюз на этом сервере

+
Проверяю…
+
+ Установить или обновить ИШ +

Дистрибутив ИШ для Linux выпускает НРД. bj скачает его с сайта НРД сам или возьмёт загруженный архив igate-…-amd64.zip. Установка занимает 2–5 минут: пакеты для работы без графики, пользователь igate, база ИШ, служба. Настройки ИШ сохраняются, прежняя версия копируется в /var/backups.

+
+ + +
+
+ + +
+
+ +
+
+ +
+

Контуры НРД

+

Сначала работают на тестовом контуре TEST3; когда переводы на нём проходят — переходят на боевой. У каждого контура свой ключ (в мастере установки ключа выберите контур) и свой канал в ИШ. bj работает с одним контуром — активным.

+ +
+ +
+
+
+ +{{end}}