From b4cfa65699f57993b65878165d10f193e2399993 Mon Sep 17 00:00:00 2001 From: zuevav Date: Tue, 6 Oct 2026 17:30:58 +0300 Subject: [PATCH] =?UTF-8?q?=D0=98=D0=A8=20=D1=81=D1=82=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=81=D1=8F=20=D0=BF=D1=80=D0=B8=20=D0=BF=D0=BE=D0=B4?= =?UTF-8?q?=D0=B3=D0=BE=D1=82=D0=BE=D0=B2=D0=BA=D0=B5;=20=D0=BD=D0=B0=20?= =?UTF-8?q?=D1=88=D0=B0=D0=B3=D0=B5=20=C2=AB=D0=A8=D0=BB=D1=8E=D0=B7=20?= =?UTF-8?q?=D0=9D=D0=A0=D0=94=C2=BB=20=E2=80=94=20=D0=B2=D0=B5=D1=80=D1=81?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=B8=20=C2=AB=D0=A3=D1=81=D1=82=D0=B0=D0=BD?= =?UTF-8?q?=D0=BE=D0=B2=D0=B8=D1=82=D1=8C=20=D0=B7=D0=B0=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=BE=C2=BB;=20=D1=81=D1=81=D1=8B=D0=BB=D0=BA=D0=B0=20=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=9D=D0=A0=D0=94=20=D0=B2=D0=BC=D0=B5=D1=81=D1=82?= =?UTF-8?q?=D0=BE=20=D0=BF=D0=B8=D1=81=D0=B5=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - «Подготовка модулей»: ИШ нет на сервере — bj ставит его из дистрибутива на сервере (образ или обновление bj — версия, проверенная ZETIT), ход — в списке. - Шаг «Шлюз НРД»: установлен и работает — версия; не стоит или не работает — одна кнопка «Установить ИШ» / «Установить ИШ заново» (систему переставлять не нужно). «Скачать с сайта НРД» и «Установить из файла» — только в режиме разработки: версию ИШ меняем после своей проверки, вместе с обновлением bj. - Каналы TEST3 и боевого: до установки ИШ — «канал создаётся после установки ИШ», после — «Создать канал». - «Письма в НРД о доступе к контурам» убраны: НРД подключает заявкой на сайте — ссылка на «MOEX МОСТ для M2M», раздел «Как подключиться». Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi --- internal/lkgateway/igate.go | 31 +++++++++ internal/lkgateway/onboarding_test.go | 32 ++++++++- internal/lkgateway/prepare.go | 66 ++++++++++++++++++- .../lkgateway/web/templates/admin_wizard.html | 7 +- .../lkgateway/web/templates/part_igate.html | 37 ++++++----- 5 files changed, 150 insertions(+), 23 deletions(-) diff --git a/internal/lkgateway/igate.go b/internal/lkgateway/igate.go index 3eaa3ec..0379c7e 100644 --- a/internal/lkgateway/igate.go +++ b/internal/lkgateway/igate.go @@ -329,6 +329,37 @@ func refreshCertStore(rc *RuntimeConfig) { } // igateBundledVersion — версия ИШ, лежащего на сервере с образа (/var/lib/bj/igate-dist; "" — нет). +// igateInstallState — ход установки ИШ (файл пишет установщик bj-igate-admin). +type igateInstallState struct { + Stage, Msg string + Busy bool // установка идёт (не done/error и файл обновлялся недавно) +} + +// igateInstallStatusPath — файл хода установки; var — для тестов. +var igateInstallStatusPath = "/var/lib/bj/igate-install-status.json" + +func igateInstallStatus() igateInstallState { + p := igateInstallStatusPath + var st igateInstallState + b, err := os.ReadFile(p) + if err != nil { + return st + } + var raw struct{ Stage, Msg string } + if json.Unmarshal(b, &raw) != nil { + return st + } + st.Stage, st.Msg = raw.Stage, raw.Msg + if fi, err := os.Stat(p); err == nil && st.Stage != "done" && st.Stage != "error" { + // установщик обновляет файл на каждом этапе; молчит 20 минут — установка прервалась + st.Busy = time.Since(fi.ModTime()) < 20*time.Minute + if !st.Busy { + st.Stage, st.Msg = "error", "установка прервалась ("+st.Msg+")" + } + } + return st +} + func igateBundledVersion() string { m, _ := filepath.Glob("/var/lib/bj/igate-dist/igate-*-amd64.zip") best := "" diff --git a/internal/lkgateway/onboarding_test.go b/internal/lkgateway/onboarding_test.go index e3427a1..f9108b5 100644 --- a/internal/lkgateway/onboarding_test.go +++ b/internal/lkgateway/onboarding_test.go @@ -314,8 +314,9 @@ func TestPrepareScreen(t *testing.T) { } } rc, _ := NewRuntimeConfig(t.TempDir() + "/setup.json") - if items := prepareItems(rc, false, false, ""); len(items) != 2 || items[0].Title != "Обновление bj" || items[0].State != "wait" || items[1].State != "ok" { - t.Fatalf("без лицензии — обновление (ещё не проверялось) и ядро: %+v", items) + // ИШ — у любой установки: ставится при подготовке из проверенного дистрибутива на сервере + if items := prepareItems(rc, false, false, ""); len(items) != 3 || items[0].Title != "Обновление bj" || items[0].State != "wait" || items[1].State != "ok" || !strings.Contains(items[2].Title, "Интеграционный шлюз") { + t.Fatalf("без лицензии — обновление (ещё не проверялось), ядро и ИШ: %+v", items) } // выпуск, который подготовка уже ставила, после перезапуска bj не ставится по кругу oldTried := prepareTriedFile @@ -730,3 +731,30 @@ func TestQuikModeAndCertHint(t *testing.T) { t.Fatal("ошибка сертификата распознаётся, ошибка логина — нет") } } + +// Ход установки ИШ для «Подготовки модулей»: идёт — пока установщик обновляет файл; молчит 20 минут — прервалась. +func TestIgateInstallStatus(t *testing.T) { + old := igateInstallStatusPath + igateInstallStatusPath = filepath.Join(t.TempDir(), "st.json") + defer func() { igateInstallStatusPath = old }() + if st := igateInstallStatus(); st.Busy || st.Stage != "" { + t.Fatalf("нет файла: %+v", st) + } + write := func(stage string, age time.Duration) { + _ = os.WriteFile(igateInstallStatusPath, []byte(`{"stage":"`+stage+`","msg":"шаг"}`), 0o644) + at := time.Now().Add(-age) + _ = os.Chtimes(igateInstallStatusPath, at, at) + } + write("package", time.Minute) + if st := igateInstallStatus(); !st.Busy { + t.Fatalf("идёт: %+v", st) + } + write("package", 25*time.Minute) + if st := igateInstallStatus(); st.Busy || st.Stage != "error" || !strings.Contains(st.Msg, "прервалась") { + t.Fatalf("прервалась: %+v", st) + } + write("done", time.Hour) + if st := igateInstallStatus(); st.Busy || st.Stage != "done" { + t.Fatalf("готово: %+v", st) + } +} diff --git a/internal/lkgateway/prepare.go b/internal/lkgateway/prepare.go index 93fa290..70707a0 100644 --- a/internal/lkgateway/prepare.go +++ b/internal/lkgateway/prepare.go @@ -7,6 +7,7 @@ package lkgateway import ( "context" + "fmt" "net/http" "os" "os/exec" @@ -30,6 +31,7 @@ var prepareRun struct { Running, Finished bool Result, Err string UpdState, UpdText string // этап «Обновление bj»: пусто — ещё не проверялось в этом запуске bj + IgateErr string // установку ИШ не удалось даже запустить } // prepareBoot — метка запуска bj: страница видит перезапуск (после обновления ядра) и продолжает подготовку. @@ -86,6 +88,7 @@ func prepareItems(rc *RuntimeConfig, running, finished bool, runErr string) []Pr } out = append(out, it) } + out = append(out, prepareIgateItem()) if moduleAllowed(rc, "web-cabinet") { out = append(out, PrepareItem{Title: moduleTitles["web-cabinet"], State: "info", Detail: "ставится на веб-сервер организации: дистрибутив и ключ кабинета — в мастере, глава «Запуск»"}) } @@ -169,7 +172,7 @@ func prepareStart(rc *RuntimeConfig) { prepareRun.Running, prepareRun.Finished, prepareRun.Result, prepareRun.Err = true, false, "", "" prepareRun.Unlock() go func() { - ctx, cancel := context.WithTimeout(context.Background(), 12*time.Minute) + ctx, cancel := context.WithTimeout(context.Background(), 25*time.Minute) defer cancel() prepareUpdates(ctx, rc) // сначала — обновления (при обновлении ядра bj перезапустится, страница продолжит) var msg, errText string @@ -180,12 +183,73 @@ func prepareStart(rc *RuntimeConfig) { } else { msg = m } + prepareIgate(ctx) prepareRun.Lock() prepareRun.Running, prepareRun.Finished, prepareRun.Result, prepareRun.Err = false, true, msg, errText prepareRun.Unlock() }() } +// prepareIgate — интеграционный шлюз НРД при подготовке: нет на сервере — поставить из дистрибутива, который +// лежит на сервере (образ или обновление bj): это версия, проверенная ZETIT вместе с bj. С сайта НРД ИШ не +// качается — новые версии приходят только с обновлением bj. Ход установки пишет установщик (igateInstallStatus). +func prepareIgate(ctx context.Context) { + if igateInstalled() || igateBundledVersion() == "" { + return + } + if st := igateInstallStatus(); !st.Busy { + res, err := runKeymediaOp(ctx, "igate-install", map[string]string{"extra": `{"bundled":"1"}`}) + if err == nil { + if ok, _ := res["ok"].(bool); !ok { + err = fmt.Errorf("%v", res["error"]) + } + } + if err != nil { + prepareRun.Lock() + prepareRun.IgateErr = err.Error() + prepareRun.Unlock() + return + } + } + for ctx.Err() == nil { // 2–5 минут: пакеты, пользователь igate, база, служба + time.Sleep(3 * time.Second) + if st := igateInstallStatus(); !st.Busy && st.Stage != "" { + return + } + } +} + +// prepareIgateItem — строка «Интеграционный шлюз НРД» на экране подготовки. +func prepareIgateItem() PrepareItem { + it := PrepareItem{Title: "Интеграционный шлюз НРД (ИШ)"} + st := igateInstallStatus() + prepareRun.Lock() + igErr := prepareRun.IgateErr + prepareRun.Unlock() + switch { + case st.Busy: + it.State, it.Detail = "run", "устанавливается: "+st.Msg+" (2–5 минут)" + case igateInstalled(): + it.State, it.Detail = "ok", "установлен"+pickStr(prefixed(", версия ", ishInstalledVersion()), "") + case st.Stage == "error": + it.State, it.Detail = "err", "не установился: "+st.Msg+" — повторить можно в мастере, шаг «Шлюз НРД», кнопка «Установить ИШ заново»" + case igErr != "": + it.State, it.Detail = "err", "установка не запустилась: "+igErr+" — повторить можно в мастере, шаг «Шлюз НРД»" + case igateBundledVersion() == "": + it.State, it.Detail = "warn", "дистрибутива ИШ на сервере нет — он приходит с обновлением bj" + default: + it.State, it.Detail = "wait", "будет установлен, версия "+igateBundledVersion() + } + return it +} + +func prefixed(p, v string) string { + if v == "" { + return "" + } + return p + v +} + // prepareUpdates — первый этап подготовки: есть обновление — поставить, нет — дальше. func prepareUpdates(ctx context.Context, rc *RuntimeConfig) { set := func(state, text string) { diff --git a/internal/lkgateway/web/templates/admin_wizard.html b/internal/lkgateway/web/templates/admin_wizard.html index 02dba1f..bfde931 100644 --- a/internal/lkgateway/web/templates/admin_wizard.html +++ b/internal/lkgateway/web/templates/admin_wizard.html @@ -528,12 +528,9 @@ {{if eq .Step 7}}

Шлюз НРД

-

Через интеграционный шлюз НРД (ИШ) на этом сервере bj отправляет переводы и получает ответы. bj ставит и настраивает шлюз сам; доступ к тестовому и боевому контуру НРД открывает по заявке — письма готовы.

+

Через интеграционный шлюз НРД (ИШ) на этом сервере bj отправляет переводы и получает ответы. bj ставит и настраивает шлюз сам; доступ к тестовому и боевому контуру НРД открывает по заявке на своём сайте — как её подать, ниже.

{{template "igateInstall" .}} -
Письма в НРД о доступе к контурам - {{template "letterCard" (letter "nsd-test")}} - {{template "letterCard" (letter "nsd-prod")}} -
+
Доступ к контурам НРД — заявкой на сайте НРД, письмами НРД не подключает. Страница сервиса MOEX МОСТ для M2M, раздел «Как подключиться»: заявка на тестирование — система МОСТ, тип «M2M Автотестирование», затем «M2M Подключение»; после тестирования — договор с НРД и боевой контур.
Дополнительно: адрес шлюза (обычно не нужно)
diff --git a/internal/lkgateway/web/templates/part_igate.html b/internal/lkgateway/web/templates/part_igate.html index 5fbea1b..6afa780 100644 --- a/internal/lkgateway/web/templates/part_igate.html +++ b/internal/lkgateway/web/templates/part_igate.html @@ -13,11 +13,12 @@

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

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

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

- -
+

Версия ИШ — та, что проверена ZETIT вместе с bj; новые версии приходят с обновлением bj. bj ставит шлюз сам при подготовке.

+ +
+ + {{if devOn}}
Для разработчиков: другая версия ИШ +
@@ -25,9 +26,7 @@
-
- -
+
{{end}}
@@ -58,16 +57,22 @@ 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+')'; - if(d.bundled){ document.getElementById('ig-bundled').style.display='block'; document.getElementById('ig-bnd-v').textContent='версия '+d.bundled+' — дистрибутив НРД записан на сервер при установке с образа'; } + var ri=document.getElementById('ig-reinst'), bb=document.getElementById('ig-bnd'), bv=document.getElementById('ig-bnd-v'); + var running=d.installed&&d.engine==='Running'; + if(!d.installed) head.innerHTML=bad('ИШ на этом сервере не установлен'); + else head.innerHTML=(running?ok('ИШ '+d.version+' установлен и работает'):bad('ИШ '+d.version+' установлен, но не работает: движок '+(d.engine||'не отвечает')+', служба '+d.service)); + // не стоит или не работает — одна кнопка: поставить заново проверенную версию с сервера (систему переставлять не нужно) + ri.style.display=running?'none':'block'; + 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 («Настройка → Обновления»).'; } + } 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('нет депозитарного кода (мастер → «Организация»)')); - else if(!ch){ s.push('канала в ИШ нет'); if(d.installed) a.push(''); } + else if(!ch){ if(d.installed){ s.push('канала в ИШ нет — bj создаст его: адрес и сертификат контура НРД известны'); a.push(''); } else s.push('канал создаётся после установки ИШ'); } else { s.push(ok('канал '+ch.code)+' '+esc(ch.crypto+' · '+ch.endpoint)+''); s.push(ch.profile?ok('ключ: профиль '+ch.profile):bad('ключ этого контура не установлен')); @@ -108,12 +113,14 @@ if(Date.now()-t0>1200000) clearInterval(iv); },3000); } document.getElementById('ig-bnd').onclick=function(){ var p=new URLSearchParams(); p.set('bundled','1'); 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-dl').onclick=function(){ var p=new URLSearchParams(); p.set('version',document.getElementById('ig-ver').value.trim()); + if(document.getElementById('ig-dl')) 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; } + if(document.getElementById('ig-up')) 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(); + // установка уже идёт (например, её начала «Подготовка модулей») — показать ход + fetch('/admin/setup/igate/install-status',{cache:'no-store'}).then(function(r){return r.json()}).then(function(s){ if(s.stage&&s.stage!=='done'&&s.stage!=='error'){ out.textContent='Установка: '+s.msg; poll(); } }).catch(function(){}); })(); {{end}}