From e4d7e31f7dc38b93138452973c2e63b1c7d117a3 Mon Sep 17 00:00:00 2001 From: fontvielle Date: Wed, 21 Oct 2026 20:15:10 +0300 Subject: [PATCH] =?UTF-8?q?=D0=98=D0=A8:=20=D1=81=D1=82=D1=80=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=86=D0=B0=20=D0=B1=D0=BB=D0=BE=D0=BA=D0=B8=D1=80=D1=83?= =?UTF-8?q?=D0=B5=D1=82=D1=81=D1=8F=20=D0=BD=D0=B0=20=D0=B2=D1=80=D0=B5?= =?UTF-8?q?=D0=BC=D1=8F=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F,=20=D0=B0=D0=B2=D1=82=D0=BE=D0=BE=D1=82=D0=BA?= =?UTF-8?q?=D0=B0=D1=82=20=D0=BF=D1=80=D0=B8=20=D1=81=D0=B1=D0=BE=D0=B5,?= =?UTF-8?q?=20=D1=87=D0=B5=D1=81=D1=82=D0=BD=D1=8B=D0=B5=20=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D1=82=D1=83=D1=81=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Обновление ИШ из обновления bj: страница «Шлюз НРД» (мастер и «Настройка → НРД») перекрывается окном с ходом установки; успех — перезагрузка страницы, ошибка — текст и журнал. Статус ИШ отдаёт upgrade_to (получена версия, обновится сам) и installing (идёт установка) — вместо «ИШ не работает» и кнопки переустановки. - Сбой после полного бэкапа (dpkg, запуск, движок не в Running) — bj-igate-admin сам откатывает ИШ к прежней версии (этап rollback); итог — ошибка «обновление до X не прошло: …, ИШ автоматически откатан к Y и работает». Проверено на 28 искусственным сбоем после установки пакета. - Откат не опускает счётчики id в базе ИШ: иначе новые пакеты получали бы уже виденные bj номера и поллер их пропускал. - Автообновление ИШ после перезапуска ядра — через 20 с (было 2 мин), с повторами через 2 и 10 мин, если дистрибутив дописан позже. В итоге обновления bj — пояснение, что сам шлюз обновится через несколько минут. - Строка версий над разделом НРД обновляется сразу при смене версии ИШ. - Сводка «НРД → Организация» и раскрытие «Перенастроить» смотрят организацию так же, как bj (orgEff: введённое или из справочника по коду) — не «не задана», когда ИНН взят из справочника. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn --- deploy/linux/sbin/bj-igate-admin | 22 +++++++++-- deploy/release/component-versions.json | 12 +++--- internal/lkgateway/admin.go | 8 ++++ internal/lkgateway/igate.go | 25 ++++++++++++- internal/lkgateway/server.go | 12 ++++-- internal/lkgateway/updater.go | 14 ++++++- .../lkgateway/web/templates/admin_setup.html | 2 +- .../lkgateway/web/templates/part_igate.html | 37 ++++++++++++++++++- .../lkgateway/web/templates/part_status.html | 4 +- 9 files changed, 115 insertions(+), 21 deletions(-) diff --git a/deploy/linux/sbin/bj-igate-admin b/deploy/linux/sbin/bj-igate-admin index cb82816..14fadb6 100755 --- a/deploy/linux/sbin/bj-igate-admin +++ b/deploy/linux/sbin/bj-igate-admin @@ -180,6 +180,13 @@ PY cp -a "$UNIT" "$BK/" 2>/dev/null; cp -a "$UNIT.d" "$BK/" 2>/dev/null; cp -a "$WRAPPER" "$BK/" 2>/dev/null igate_version > "$BK/version" echo "$BK" > /var/backups/ish-last-backup.txt; echo "резервная копия прежнего ИШ $(cat "$BK/version"): $BK" + [ -s "$BK/var-lib-igate.tar.gz" ] && [ -s "$BK/igate-db.dump" ] && [ -s "$BK/opt-nsd-igate.tar.gz" ] && FULLBK=$BK + fi + if [ -n "${FULLBK:-}" ]; then + # обновление поверх работающего ИШ: при любой ошибке дальше — вернуть прежнюю версию (этап rollback, не error: + # страница продолжает показывать ход, итог — ошибка с пометкой «откатан») + newv=$(basename "$deb" | sed -E "s/^igate_([0-9.]+)-([0-9]+)_.*/\\1.\\2/") # igate_110.0-997_amd64.deb → 110.0.997 + die() { st rollback "$1 — откатываю к прежней версии"; echo "== ошибка обновления: $1 — откат"; BJ_RB_REASON="$1" BJ_RB_TO="$newv" /usr/local/sbin/bj-igate-admin rollback-now; exit 1; } fi echo "igate igate/service boolean false" | debconf-set-selections echo "igate igate/shortcut boolean false" | debconf-set-selections @@ -268,14 +275,14 @@ SQL ok "откат ИШ к $(cat "$BK/version") запущен" ;; rollback-now) - LOG=/var/log/bj-igate-install.log; : > "$LOG" + LOG=/var/log/bj-igate-install.log; [ -n "${BJ_RB_REASON:-}" ] || : > "$LOG" st() { python3 - "$STATUS" "$1" "$2" "$LOG" <<'PY' import json,sys,datetime tail=open(sys.argv[4],errors='replace').read()[-1500:] json.dump({"at":datetime.datetime.now().astimezone().isoformat(timespec="seconds"),"stage":sys.argv[2],"msg":sys.argv[3],"log":tail},open(sys.argv[1],"w"),ensure_ascii=False) PY chown bj:bj "$STATUS" 2>/dev/null; echo "== $2" >> "$LOG"; } - die() { st error "$1"; exit 1; } + die() { st error "${BJ_RB_REASON:+Обновление ИШ не прошло ($BJ_RB_REASON), и откат не удался: }$1"; exit 1; } exec >>"$LOG" 2>&1 BK=$(cat /var/backups/ish-last-backup.txt 2>/dev/null); old=$(cat "$BK/version" 2>/dev/null); cur=$(igate_version) [ -n "$old" ] || die "нет резервной копии прежнего ИШ" @@ -290,9 +297,14 @@ PY [ -d "$BK/$(basename "$UNIT").d" ] && { rm -rf "$UNIT.d"; cp -a "$BK/$(basename "$UNIT").d" "$UNIT.d"; } [ -f "$BK/$(basename "$WRAPPER")" ] && cp -a "$BK/$(basename "$WRAPPER")" "$WRAPPER" st db "возврат базы ИШ" + # текущие счётчики id: после отката не опускать их ниже — иначе ИШ выдаст новым пакетам уже виденные bj номера + seqs=$(psqlq -c "select sequencename||' '||coalesce(last_value,0) from pg_sequences where schemaname='public'" 2>/dev/null) runuser -u postgres -- dropdb --if-exists "$DB" && runuser -u postgres -- createdb -O "$SVC_USER" "$DB" || die "не пересоздана база ИШ" runuser -u postgres -- pg_restore -d "$DB" "$BK/igate-db.dump" || true psqlq -c "select to_regclass('public.parameters') is not null" | grep -q t || die "база ИШ не восстановилась из $BK/igate-db.dump" + printf '%s\n' "$seqs" | while read -r n v; do + [ -n "$n" ] && psqlq -c "select setval('public.\"$n\"', greatest($v, (select last_value from public.\"$n\")))" >/dev/null 2>&1 + done # пришедший с обновлением дистрибутив больше не ставить сам (иначе bj вернёт версию сразу); новая версия — снова автоматически dv=$(python3 -c "import json;print(json.load(open('/var/lib/bj/.bj/components.json')).get('igate-dist',{}).get('version',''))" 2>/dev/null) [ -n "$dv" ] && { printf '%s @@ -300,7 +312,11 @@ PY st start "запуск ИШ $old" systemctl daemon-reload; systemctl restart igate || die "служба igate не запустилась" wait_engine 120 || die "ИШ $old запущен, но движок не перешёл в Running за 2 минуты. Журнал ИШ: $DATA_DIR/logs-svc/log.txt" - st done "ИШ откатан с $cur к $old и работает" ;; + if [ -n "${BJ_RB_REASON:-}" ]; then + st error "Обновление ИШ${BJ_RB_TO:+ до $BJ_RB_TO} не прошло: $BJ_RB_REASON. ИШ автоматически откатан к $old и работает; настройки и каналы прежние." + else + st done "ИШ откатан с $cur к $old и работает" + fi ;; contour) contour=$(j contour); crypto=$(j crypto); dep=$(j deponent); prof=$(j profile) diff --git a/deploy/release/component-versions.json b/deploy/release/component-versions.json index 39252a6..5642a66 100644 --- a/deploy/release/component-versions.json +++ b/deploy/release/component-versions.json @@ -1,11 +1,11 @@ { "bj-component-install": { - "fingerprint": "16a300f65063aaee25793158b20d42fed527c7212f9f632b4076ad476b2ed9fb", - "version": "2026.10.21.6" + "fingerprint": "5be608b8b424a076d2732b3ca0943b47d7a509a494285cd5d4fab96f1e997765", + "version": "2026.10.21.17" }, "bj-installer": { - "fingerprint": "6ef45b6928de126dd28ba9bdd2c5da7871a7d18fe76e5f90b5962c55db4a0c47", - "version": "2026.10.21.6" + "fingerprint": "3e893a6143ad7476e3ac45a0b76a2edaee72b07ad0223901d527e32f61e317fb", + "version": "2026.10.21.17" }, "bj-lk": { "fingerprint": "48a6cac59febba3392c854423b729f426383d86d35514578b5546786c8ab3355", @@ -20,8 +20,8 @@ "version": "2026.10.21.6" }, "bj-server": { - "fingerprint": "5617c24adb2d2392aecdfe6711bff54e8936688c249408c22042174511f258ba", - "version": "2026.10.21.16" + "fingerprint": "56630a5e3e2bd9e4a4f829f8351eb01ca9049ce0287f16449f0961b150c9ef85", + "version": "2026.10.21.17" }, "crypto-service.jar": { "fingerprint": "2c4fd536bc9b1c20c69a70e8dcd374a61847b263aeb2e474c45debb01fbd8233", diff --git a/internal/lkgateway/admin.go b/internal/lkgateway/admin.go index 6299d66..d001985 100644 --- a/internal/lkgateway/admin.go +++ b/internal/lkgateway/admin.go @@ -37,6 +37,14 @@ type admin struct { // русификация статусов и других технических обозначений (см. требование // «всё UI на русском, кроме программных терминов»). var templateFuncs = template.FuncMap{ + // orgEff — наша организация так, как её использует bj: введённое вручную, иначе из справочника участников по коду. + "orgEff": func() map[string]string { + if globalRC == nil { + return map[string]string{} + } + s := globalRC.Snapshot() + return map[string]string{"Name": orgName(s), "INN": orgINN(s), "Code": orgCode(s)} + }, // Карточка «Регистрация участника M2M» для «Исходящих»: когда отправили анкету и вердикт НРД. "participantRegHTML": func() template.HTML { reg := loadParticipantReg() diff --git a/internal/lkgateway/igate.go b/internal/lkgateway/igate.go index 38c2f3f..a5c7275 100644 --- a/internal/lkgateway/igate.go +++ b/internal/lkgateway/igate.go @@ -167,6 +167,13 @@ func registerIgateAdmin(mux *http.ServeMux, rc *RuntimeConfig) { res["depo_db_test"], res["depo_db_prod"] = s.NSD.DepoDBTest, s.NSD.DepoDBProd res["bundled"] = igateBundledVersion() res["keymedia"] = keymediaState() // missing — ИШ ждёт флешку с ключом: переустановка не поможет + res["upgrade_to"] = igatePendingUpgrade() + if v, _ := res["version"].(string); v != "" && canonVer(systemVersions(rc).Get("ish").Version) != canonVer(v) { + go refreshVersions(rc) // ИШ обновили/откатили — строка версий над разделом не должна показывать прежнюю + } + if st := igateInstallStatus(); st.Busy { + res["installing"] = st.Msg + } jsonOut(w, res) }) // установка или обновление ИШ: загруженный архив НРД или версия для скачивания с nsd.ru @@ -474,14 +481,28 @@ func igateInstallDist(ctx context.Context) error { // igateAutoUpgradeSkipPath — версия дистрибутива, с которой ИШ откатили (её автоматически не ставим). var igateAutoUpgradeSkipPath = "/var/lib/bj/igate-autoupgrade-skip" +func igateUpgradeSkipped(dist string) bool { + b, err := os.ReadFile(igateAutoUpgradeSkipPath) + return err == nil && strings.TrimSpace(string(b)) == dist +} + +// igatePendingUpgrade — версия ИШ, до которой bj обновит шлюз сам (пришла с обновлением bj); "" — нечего. +func igatePendingUpgrade() string { + cur := ishInstalledVersion() + _, dist := igateDistFile() + if !igateInstalled() || cur == "" || dist == "" || !release.IsNewer(dist, cur) || igateUpgradeSkipped(dist) { + return "" + } + return dist +} + func igateAutoUpgrade(ctx context.Context, rc *RuntimeConfig) { cur := ishInstalledVersion() _, dist := igateDistFile() if !igateInstalled() || cur == "" || dist == "" || !release.IsNewer(dist, cur) || igateInstallStatus().Busy { return } - // ИШ откатили с этой версии вручную — сам не ставим, пока не придёт следующая - if b, err := os.ReadFile(igateAutoUpgradeSkipPath); err == nil && strings.TrimSpace(string(b)) == dist { + if igateUpgradeSkipped(dist) { // ИШ откатили с этой версии вручную — сам не ставим, пока не придёт следующая return } log.Printf("lk-gateway: ИШ %s старее проверенного дистрибутива %s — обновляю", cur, dist) diff --git a/internal/lkgateway/server.go b/internal/lkgateway/server.go index 9d31c84..09d6b92 100644 --- a/internal/lkgateway/server.go +++ b/internal/lkgateway/server.go @@ -376,10 +376,14 @@ func (s *Server) Run(ctx context.Context) error { go publicIPLoop(ctx) // внешний адрес сервера — для письма в ARQA (у сервера лицензий, раз в час) // ИШ старее дистрибутива из обновления bj (ядро перезапускалось при обновлении) — обновить go func() { - select { - case <-ctx.Done(): - case <-time.After(2 * time.Minute): - igateAutoUpgrade(ctx, s.rc) + // root-установщик мог дописать дистрибутив уже после перезапуска ядра — проверяем несколько раз + for _, wait := range []time.Duration{20 * time.Second, 2 * time.Minute, 10 * time.Minute} { + select { + case <-ctx.Done(): + return + case <-time.After(wait): + igateAutoUpgrade(ctx, s.rc) + } } }() diff --git a/internal/lkgateway/updater.go b/internal/lkgateway/updater.go index c959b43..8f7ce19 100644 --- a/internal/lkgateway/updater.go +++ b/internal/lkgateway/updater.go @@ -252,6 +252,16 @@ func (u *Updater) ApplyUpdate(ctx context.Context, names []string) (string, erro return msg, err } +// igateUpgradeNote — с обновлением пришёл дистрибутив ИШ: сам шлюз bj обновит чуть позже, отдельно. +func igateUpgradeNote(list []string) string { + for _, x := range list { + if v, ok := strings.CutPrefix(x, "igate-dist "); ok { + return "; пришёл дистрибутив ИШ " + v + " — сам шлюз bj обновит автоматически в течение нескольких минут (около 2 минут шлюз недоступен; настройки и каналы сохраняются)" + } + } + return "" +} + func (u *Updater) applyUpdate(ctx context.Context, names []string) (string, error) { if licensingEnabled(u.rc) { ls := licenseStatus(u.rc) @@ -392,7 +402,7 @@ func (u *Updater) applyUpdate(ctx context.Context, names []string) (string, erro if err != nil { return "", fmt.Errorf("установка: %w", err) } - msg := "установлено: " + strings.Join(list, ", ") + msg := "установлено: " + strings.Join(list, ", ") + igateUpgradeNote(list) if r, ok := res["restarted"].([]any); ok && len(r) > 0 { var units []string for _, x := range r { @@ -772,7 +782,7 @@ func (u *Updater) applyBundle(ctx context.Context, zipPath string) (string, erro if err != nil { return "", fmt.Errorf("установка: %w", err) } - msg := fmt.Sprintf("из файла (релиз %s) установлено: %s", m.Version, strings.Join(list, ", ")) + msg := fmt.Sprintf("из файла (релиз %s) установлено: %s", m.Version, strings.Join(list, ", ")) + igateUpgradeNote(list) if r, ok := res["restarted"].([]any); ok && len(r) > 0 { var units []string for _, x := range r { diff --git a/internal/lkgateway/web/templates/admin_setup.html b/internal/lkgateway/web/templates/admin_setup.html index 1024cef..7a15131 100644 --- a/internal/lkgateway/web/templates/admin_setup.html +++ b/internal/lkgateway/web/templates/admin_setup.html @@ -350,7 +350,7 @@ {{template "verline" (.Versions.Get "ish")}} {{template "nsdStatus" .}}
-
+
Перенастроитьорганизация, реквизиты в НРД, кто отвечает на переводы, шлюз и контуры, папки шлюза по сети {{template "orgSection" .}} {{template "nsdRequisites" .}} diff --git a/internal/lkgateway/web/templates/part_igate.html b/internal/lkgateway/web/templates/part_igate.html index 051d457..c633282 100644 --- a/internal/lkgateway/web/templates/part_igate.html +++ b/internal/lkgateway/web/templates/part_igate.html @@ -52,6 +52,14 @@
Проверяю…

Версию ИШ проверяет ZETIT; новые приходят с обновлением bj.

+
@@ -258,6 +266,12 @@ if(!running && d.keymedia==='missing'){ head.innerHTML='⚠ ИШ '+esc(d.version)+' ждёт флешку с ключом подписи — её нет в сервере. Вставьте флешку: bj перезапустит шлюз сам. Переустанавливать ИШ не нужно.'; } // не стоит или не работает — одна кнопка: поставить заново проверенную версию с сервера (систему переставлять не нужно) ri.style.display=(running||d.keymedia==='missing')?'none':'block'; // без флешки переустановка не поможет + if(d.installing){ // bj сам ставит/обновляет ИШ: остановка шлюза — штатная, переустанавливать не нужно + head.innerHTML='⟳ Идёт установка ИШ'+(d.upgrade_to?' '+esc(d.upgrade_to):'')+' — '+esc(d.installing)+'. Шлюз недоступен 1–3 минуты; настройки и каналы сохраняются.'; ri.style.display='none'; + } else if(d.upgrade_to){ + head.innerHTML+='
⟳ Получена версия ИШ '+esc(d.upgrade_to)+' (проверена ZETIT) — bj обновит шлюз сам в ближайшие минуты; настройки и каналы сохранятся.
'; + } + if(d.installing||d.upgrade_to) ovStart(); // обновление идёт или вот-вот начнётся — страница перекрыта ходом if(!running){ if(d.bundled){ bb.style.display=''; bb.textContent=d.installed?'Установить ИШ заново':'Установить ИШ'; bv.textContent='версия '+d.bundled+' — проверена ZETIT, дистрибутив на сервере'; } else { bb.style.display='none'; bv.textContent='Дистрибутива ИШ на сервере нет — он приходит с обновлением bj («Настройка → Обновления»).'; } @@ -334,7 +348,28 @@ note='идёт '+mmss(el)+' · обычно до '+mmss(x[2])+(slow?' — дольше обычного; установка работает, пока счётчик идёт; зависшая снимается сама через 40 минут':'')+''; } return '
  • '+esc(x[1])+note+'
  • '; }).join(''); } - function poll(){ var t0=Date.now(), btn=document.getElementById('ig-bnd'); + // окно поверх страницы на время установки/обновления/отката ИШ + var ov={on:false,seen:false,t0:0,iv:null}; + function ovSet(h,m,log,fin){ var o=document.getElementById('ig-ov'); o.style.display='flex'; + document.getElementById('ig-ov-h').innerHTML=h; document.getElementById('ig-ov-m').textContent=m||''; + var l=document.getElementById('ig-ov-log'); if(log){ l.style.display='block'; l.textContent=log; l.scrollTop=l.scrollHeight; } else l.style.display='none'; + document.getElementById('ig-ov-btns').style.display=fin?'block':'none'; } + function ovStart(){ if(ov.on) return; ov.on=true; ov.t0=Date.now(); ovTick(); ov.iv=setInterval(ovTick,3000); } + function ovTick(){ + fetch('/admin/setup/igate/install-status',{cache:'no-store'}).then(function(r){return r.json()}).then(function(s){ + var run=s.stage&&s.stage!=='done'&&s.stage!=='error'; + if(run){ ov.seen=true; ovSet(s.stage==='rollback'?'⟲ Обновление не прошло — возвращаю прежнюю версию ИШ':'⟳ Идёт установка шлюза НРД', + (s.msg||s.stage)+' · '+(s.elapsed||0)+' с на этапе. Не закрывайте страницу: обмен с НРД на 1–3 минуты приостановлен, настройки и каналы сохраняются.', s.log, false); return; } + if(!ov.seen){ + if(Date.now()-ov.t0>15*60000){ clearInterval(ov.iv); ov.on=false; document.getElementById('ig-ov').style.display='none'; return; } + ovSet('⟳ Получено обновление шлюза НРД','bj начнёт обновление автоматически в течение минуты. Не закрывайте страницу.','',false); return; } + clearInterval(ov.iv); + if(s.stage==='done'){ ovSet('✓ '+esc(s.msg)+'','Страница обновится…','',false); setTimeout(function(){ location.reload(); },2500); } + else ovSet('✗ Установка шлюза не прошла', s.msg, s.log, true); + }).catch(function(){}); + } + document.getElementById('ig-ov-close').onclick=function(){ location.reload(); }; + function poll(){ ovStart(); var t0=Date.now(), btn=document.getElementById('ig-bnd'); btn.disabled=true; // пока идёт установка — второй раз не нажать clearInterval(tick); tick=setInterval(draw,1000); var iv=setInterval(function(){ diff --git a/internal/lkgateway/web/templates/part_status.html b/internal/lkgateway/web/templates/part_status.html index 82914ce..a06be4f 100644 --- a/internal/lkgateway/web/templates/part_status.html +++ b/internal/lkgateway/web/templates/part_status.html @@ -34,13 +34,13 @@
    {{end}} -{{define "nsdStatus"}}{{$n := .Settings.NSD}}{{$a := .Settings.M2MAuto}}{{$org := and $n.OrgINN $n.OrgCode}}{{$req := and $n.DeponentCode $n.AccountID $n.SectionID}}{{$ok := and $org $req $n.IGWBaseURL}} +{{define "nsdStatus"}}{{$n := .Settings.NSD}}{{$a := .Settings.M2MAuto}}{{$o := orgEff}}{{$org := and (index $o "INN") (index $o "Code")}}{{$req := and $n.DeponentCode $n.AccountID $n.SectionID}}{{$ok := and $org $req $n.IGWBaseURL}}
    {{if $ok}}Обмен с НРД настроен{{else}}Обмен с НРД настроен не до конца{{end}} {{if $ok}}Организация, реквизиты и шлюз — ниже. Изменить — «Перенастроить».{{else}}Что не так — в строках ниже с ✗; поправить — в «Перенастроить» или мастером настройки.{{end}}
    -
    Организация
    {{if $org}}{{if $n.OrgName}}{{$n.OrgName}}{{else}}—{{end}} · ИНН {{$n.OrgINN}} · код {{$n.OrgCode}}{{else}}✗ не задана{{end}}
    +
    Организация
    {{if $org}}{{with index $o "Name"}}{{.}}{{else}}—{{end}} · ИНН {{index $o "INN"}} · код {{index $o "Code"}}{{else}}✗ не задана{{end}}
    Реквизиты в НРД
    {{if $req}}код {{$n.DeponentCode}} · счёт {{$n.AccountID}} · раздел {{$n.SectionID}}{{else}}✗ не заданы{{end}}
    Кто отвечает на переводы
    {{if $a.Enabled}}bj сам — автоматически{{else}}Оператор — запасной режим{{end}}{{if not $a.ChangedAt.IsZero}} · с {{msk $a.ChangedAt}}{{end}}
    Шлюз НРД
    {{if $n.IGWBaseURL}}контур {{$n.Profile}} · проверяю шлюз…{{else}}✗ не настроен{{end}}