diff --git a/deploy/linux/sbin/bj-igate-admin b/deploy/linux/sbin/bj-igate-admin index 14fadb6..153a500 100755 --- a/deploy/linux/sbin/bj-igate-admin +++ b/deploy/linux/sbin/bj-igate-admin @@ -73,19 +73,22 @@ run_as_igate_keyring() { # команда в сессии igate с разбло dbus-run-session -- bash -c 'eval "$(printf "%s\n" "$(cat "$HOME/.keyring-pass")" | gnome-keyring-daemon --replace --unlock --components=secrets,pkcs11 2>/dev/null)"; export GNOME_KEYRING_CONTROL; '"$1" } +channels_json() { # каналы ИШ: код, включён, адрес НРД, ключ, крипто, депонент, папка + if runuser -u postgres -- psql -lqt 2>/dev/null | cut -d'|' -f1 | grep -qw "$DB"; then + psqlq -c "select coalesce(json_agg(json_build_object('id',c.id,'code',c.code,'enabled',c.enable, + 'endpoint',(select value from parameters p where p.chanel_id=c.id and p.name='wslEndpoint'), + 'profile',(select value from parameters p where p.chanel_id=c.id and p.name='cryptography.profile'), + 'crypto',(select value from parameters p where p.chanel_id=c.id and p.name='cryptography.type'), + 'deponent',(select value from parameters p where p.chanel_id=c.id and p.name='repositoryCode'), + 'dir',(select value from parameters p where p.chanel_id=c.id and p.name='dir')) order by c.id),'[]') from channels c" 2>/dev/null || echo "[]" + else echo "[]"; fi +} + case "$cmd" in status) inst=false; [ -x "$IGATE_DIR/igate-svc/igate-svc" ] && inst=true svc=$(systemctl is-active igate 2>/dev/null); st=$(engine_state) - chans="[]" - 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 + chans=$(channels_json) rb=""; BK=$(cat /var/backups/ish-last-backup.txt 2>/dev/null) [ -n "$BK" ] && [ -s "$BK/version" ] && [ -s "$BK/opt-nsd-igate.tar.gz" ] && [ -s "$BK/igate-db.dump" ] && [ -s "$BK/var-lib-igate.tar.gz" ] && rb=$(cat "$BK/version") echo "{\"ok\":true,\"installed\":$inst,\"version\":$(q "$(igate_version)"),\"service\":$(q "$svc"),\"engine\":$(q "$st"),\"rollback\":$(q "$rb"),\"channels\":${chans:-[]}}" ;; @@ -168,11 +171,14 @@ PY runuser -u postgres -- psql -qtAc "select pg_reload_conf()" >/dev/null fi - st install "установка пакета ИШ" + oldv=""; [ -d "$IGATE_DIR" ] && oldv=$(igate_version) + st stop "остановка ${oldv:+старого }ИШ${oldv:+ $oldv}" systemctl stop igate >/dev/null 2>&1 || true pkill -9 -u "$SVC_USER" -f igate-svc 2>/dev/null; pkill -9 -u "$SVC_USER" -f Xvfb 2>/dev/null; rm -f /tmp/CoreFxPipe_NSD.IGate.* if [ -d "$IGATE_DIR" ]; then + st backup "резервная копия ИШ $oldv (программа, данные, база)" BK=/var/backups/ish-$(date +%Y%m%d-%H%M%S); mkdir -p "$BK" + channels_json > "$BK/channels.json" # для проверки после обновления: каналы и ключи те же tar czf "$BK/opt-nsd-igate.tar.gz" -C "$(dirname "$IGATE_DIR")" "$(basename "$IGATE_DIR")" 2>/dev/null runuser -u postgres -- pg_dump -Fc "$DB" > "$BK/igate-db.dump" 2>/dev/null # для отката: данные ИШ (без флешки с ключом и кэша), юнит, обёртка запуска, версия @@ -188,6 +194,7 @@ PY newv=$(basename "$deb" | sed -E "s/^igate_([0-9.]+)-([0-9]+)_.*/\\1.\\2/") # igate_110.0-997_amd64.deb → 110.0.997 die() { st rollback "$1 — откатываю к прежней версии"; echo "== ошибка обновления: $1 — откат"; BJ_RB_REASON="$1" BJ_RB_TO="$newv" /usr/local/sbin/bj-igate-admin rollback-now; exit 1; } fi + st install "установка ${FULLBK:+нового }ИШ${newv:+ $newv}" echo "igate igate/service boolean false" | debconf-set-selections echo "igate igate/shortcut boolean false" | debconf-set-selections DEBIAN_FRONTEND=noninteractive dpkg -i --force-confnew "$deb" || { DEBIAN_FRONTEND=noninteractive apt-get -f install -y -qq || die "пакет ИШ не установился (dpkg)"; } @@ -264,7 +271,44 @@ SQL systemctl restart igate || die "служба igate не запустилась" wait_engine 120 || die "ИШ запущен, но движок не перешёл в Running за 2 минуты. Журнал ИШ: $DATA_DIR/logs-svc/log.txt" case "$(j file)" in /var/lib/bj/igate-upload/*) rm -f "$(j file)" ;; esac # загруженный архив больше не нужен (с образа — остаётся) - st done "ИШ $(igate_version) установлен и работает; дальше — канал контура (TEST3 или боевой)" ;; + if [ -n "${FULLBK:-}" ]; then + # обновление: «готово» — только когда новый ИШ проверен; не прошло — die (автооткат к прежней версии) + st verify "проверка нового ИШ: версия, служба, каналы, ключ, обмен с НРД" + v=$(igate_version) + [ "$v" = "$newv" ] || die "после установки версия ИШ $v, ожидалась $newv" + echo "✓ версия ИШ $v" + sleep 10 + [ "$(systemctl is-active igate)" = active ] && [ "$(engine_state)" = Running ] || die "ИШ $v остановился после запуска" + echo "✓ служба igate работает, движок Running" + now=$(channels_json) + chk=$(python3 - "$FULLBK/channels.json" "$now" <<'PY' +import json,sys +keys=('code','enabled','endpoint','profile','crypto','deponent','dir') +try: old=json.load(open(sys.argv[1])) +except Exception: old=None +new=json.loads(sys.argv[2] or '[]') +pick=lambda l:[{k:c.get(k) for k in keys} for c in l] +if old is not None and pick(old)!=pick(new): + print('ERR каналы ИШ после обновления не совпадают с прежними: было %s, стало %s'%(pick(old),pick(new))); sys.exit() +on=[c for c in new if c.get('enabled')] +print('OK '+'; '.join('канал %s включён, ключ %s'%(c['code'],c.get('profile') or '—') for c in on) if on else 'OK каналов не настроено') +PY +) + case "$chk" in OK*) echo "✓ ${chk#OK } — как до обновления" ;; *) die "${chk#ERR }" ;; esac + xok="" + if printf '%s' "$now" | grep -q '"enabled" : true'; then + pid=$(pgrep -u "$SVC_USER" -f "$IGATE_DIR/igate-svc/igate-svc" | tail -1) + for _ in $(seq 1 30); do + grep -a "PID_$pid " "$DATA_DIR/logs-svc/log.txt" 2>/dev/null | grep -q -- '-> OK (HTTP 200)' && { xok=1; break; } + sleep 3 + done + # НРД не ответил — не повод откатывать (старая версия связаться тоже не сможет): итог с предупреждением + [ -n "$xok" ] && echo "✓ обмен с НРД: запрос к НРД прошёл (HTTP 200)" || echo "⚠ за 90 с ИШ не получил ответа НРД — проверьте связь" + fi + st done "ИШ обновлён с $oldv до $v и проверен: служба и движок работают, каналы и ключ как до обновления${xok:+, обмен с НРД идёт}${xok:-; ответа НРД пока нет — проверьте связь}" + else + st done "ИШ $(igate_version) установлен и работает; дальше — канал контура (TEST3 или боевой)" + fi ;; rollback) pgrep -f "bj-igate-admin (install|rollback)-now" >/dev/null && err "установка или откат ИШ уже идёт" diff --git a/internal/lkgateway/igate.go b/internal/lkgateway/igate.go index a5c7275..e5e0767 100644 --- a/internal/lkgateway/igate.go +++ b/internal/lkgateway/igate.go @@ -234,20 +234,7 @@ func registerIgateAdmin(mux *http.ServeMux, rc *RuntimeConfig) { }) // ход установки: этап, сообщение, журнал и сколько секунд идёт текущий этап (по времени записи файла) mux.HandleFunc("/admin/setup/igate/install-status", func(w http.ResponseWriter, r *http.Request) { - raw, err := os.ReadFile(igateInstallStatusPath) - out := map[string]any{} - if err != nil || json.Unmarshal(raw, &out) != nil { - jsonOut(w, map[string]any{}) - return - } - if fi, err := os.Stat(igateInstallStatusPath); err == nil { - out["elapsed"] = int(time.Since(fi.ModTime()).Seconds()) - } - st := igateInstallStatus() - if st.Stage == "error" && out["stage"] != "error" { // молчит 20 минут — прервалась - out["stage"], out["msg"] = "error", st.Msg - } - jsonOut(w, out) + jsonOut(w, igateInstallStatusJSON(rc)) }) // канал контура в ИШ (адрес, отпечаток УЦ, папка обмена); ключ — мастером установки ключа mux.HandleFunc("/admin/setup/igate/contour", func(w http.ResponseWriter, r *http.Request) { @@ -410,6 +397,30 @@ type igateInstallState struct { // igateInstallStatusPath — файл хода установки; var — для тестов. var igateInstallStatusPath = "/var/lib/bj/igate-install-status.json" +// igateInstallStatusJSON — ход установки/обновления ИШ для страниц: этап, сообщение, журнал, сколько +// секунд идёт этап (elapsed), идёт ли сейчас (busy). Как только установка закончилась, версии +// пересобираются тут же — строка «Интеграционный шлюз НРД (ИШ): …» не показывает прежнюю. +func igateInstallStatusJSON(rc *RuntimeConfig) map[string]any { + out := map[string]any{} + raw, err := os.ReadFile(igateInstallStatusPath) + if err != nil || json.Unmarshal(raw, &out) != nil { + return map[string]any{} + } + fi, err := os.Stat(igateInstallStatusPath) + if err == nil { + out["elapsed"] = int(time.Since(fi.ModTime()).Seconds()) + } + st := igateInstallStatus() + if st.Stage == "error" && out["stage"] != "error" { // молчит 20 минут — прервалась + out["stage"], out["msg"] = "error", st.Msg + } + out["busy"] = st.Busy + if !st.Busy && err == nil && fi.ModTime().After(systemVersions(rc).At) { + refreshVersions(rc) + } + return out +} + func igateInstallStatus() igateInstallState { p := igateInstallStatusPath var st igateInstallState diff --git a/internal/lkgateway/ish_version.go b/internal/lkgateway/ish_version.go index 2fc246e..23eada3 100644 --- a/internal/lkgateway/ish_version.go +++ b/internal/lkgateway/ish_version.go @@ -82,20 +82,24 @@ var ( // сайт НРД на каждый показ статуса). Сетевая часть best-effort: при недоступности // сайта возвращается только установленная версия. func ishVersionInfo(ctx context.Context) IshVersionInfo { + // установленную версию читаем всегда заново (ИШ мог обновиться минуту назад), сайт — из кэша ishVerMu.Lock() - if ishVerCache != nil && time.Since(ishVerCache.CheckedAt) < ishVerTTL { - c := *ishVerCache - ishVerMu.Unlock() - return c - } + site := ishVerCache ishVerMu.Unlock() - - info := IshVersionInfo{ - Installed: ishInstalledVersion(), - CheckedAt: time.Now().UTC(), + if site == nil || time.Since(site.CheckedAt) >= ishVerTTL { + linux, latest := ishSiteVersions(ctx) + site = &IshVersionInfo{SiteLinux: linux, SiteLatest: latest, CheckedAt: time.Now().UTC()} + ishVerMu.Lock() + ishVerCache = site + ishVerMu.Unlock() } - linux, latest := ishSiteVersions(ctx) - info.SiteLinux, info.SiteLatest = linux, latest + info := IshVersionInfo{ + Installed: ishInstalledVersion(), + SiteLinux: site.SiteLinux, + SiteLatest: site.SiteLatest, + CheckedAt: site.CheckedAt, + } + linux, latest := info.SiteLinux, info.SiteLatest switch { case info.Installed == "": @@ -113,10 +117,6 @@ func ishVersionInfo(ctx context.Context) IshVersionInfo { info.UpToDate = false info.Note = "доступно обновление до " + linux + " (Astra-Linux)" } - - ishVerMu.Lock() - ishVerCache = &info - ishVerMu.Unlock() return info } diff --git a/internal/lkgateway/updprogress.go b/internal/lkgateway/updprogress.go index 3c093e3..4622ea2 100644 --- a/internal/lkgateway/updprogress.go +++ b/internal/lkgateway/updprogress.go @@ -15,22 +15,27 @@ import ( ) type updateProgressState struct { - Seq int64 `json:"seq"` - Active bool `json:"active"` - Finished bool `json:"finished"` - Step int `json:"step"` - Steps int `json:"steps"` - Stage string `json:"stage"` - Done int64 `json:"done"` - Total int64 `json:"total"` - Target string `json:"target,omitempty"` // версия ядра после обновления (если ядро обновляется) - Result string `json:"result,omitempty"` - Note string `json:"note,omitempty"` // пояснение, которое нужно показать и после перезапуска ядра - Err string `json:"error,omitempty"` - Started time.Time `json:"started"` - Version string `json:"version"` // версия работающего bj-server + Seq int64 `json:"seq"` + Active bool `json:"active"` + Finished bool `json:"finished"` + Step int `json:"step"` + Steps int `json:"steps"` + Stage string `json:"stage"` + Done int64 `json:"done"` + Total int64 `json:"total"` + Target string `json:"target,omitempty"` // версия ядра после обновления (если ядро обновляется) + Result string `json:"result,omitempty"` + Note string `json:"note,omitempty"` // пояснение, которое нужно показать и после перезапуска ядра + Err string `json:"error,omitempty"` + Started time.Time `json:"started"` + Version string `json:"version"` // версия работающего bj-server + IgateTo string `json:"igate_to,omitempty"` // bj сам обновит ИШ до этой версии (ещё не обновил) + Igate map[string]any `json:"igate,omitempty"` // ход установки ИШ (как /admin/setup/igate/install-status) + Up int `json:"up"` // секунд с запуска bj-server } +var procStarted = time.Now() + var updProg = struct { sync.Mutex s updateProgressState @@ -110,6 +115,7 @@ func (h *setupHandlers) updateProgress(w http.ResponseWriter, r *http.Request) { s := updProg.s updProg.Unlock() s.Version = BuildVersion + s.IgateTo, s.Igate, s.Up = igatePendingUpgrade(), igateInstallStatusJSON(h.rc), int(time.Since(procStarted).Seconds()) w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", "no-store") _ = json.NewEncoder(w).Encode(s) diff --git a/internal/lkgateway/web/templates/layout.html b/internal/lkgateway/web/templates/layout.html index 0bc979f..13bde5e 100644 --- a/internal/lkgateway/web/templates/layout.html +++ b/internal/lkgateway/web/templates/layout.html @@ -1030,6 +1030,9 @@ button.dq-link, .dq-link { background:none !important; border:0 !important; padd

+ + @@ -1055,6 +1058,62 @@ button.dq-link, .dq-link { background:none !important; border:0 !important; padd tick(); } } + /* Обновление шлюза НРД (ИШ): с обновлением bj пришёл дистрибутив ИШ — bj ставит его сам после перезапуска. + Окно не закрывается, пока новый ИШ не установлен и не проверен; на любой странице, пока идёт, — + то же окно (страницы не показывают полуобновлённое состояние). Ошибка — текст, журнал и автооткат. */ + var IGS=[['prep','Загрузка и проверка дистрибутива ИШ'],['stop','Остановка старого ИШ'],['backup','Резервная копия старого ИШ'], + ['install','Установка нового ИШ'],['start','Запуск нового ИШ'],['verify','Проверка: версия, каналы, ключ, обмен с НРД']]; + function igKey(s){ var g=s.stage||''; if(/^(queued|deps|dist|user|db|package)$/.test(g)) return 'prep'; + if(g==='config'||g==='schema') return 'install'; return g; } + var ig={on:false,t0:0,seen:false,rb:false,idx:-1}; + function igDraw(s, waiting){ + var k=igKey(s||{}), i=-1; IGS.forEach(function(x,j){ if(x[0]===k) i=j; }); + if(!ig.rb && i>ig.idx) ig.idx=i; if(waiting) ig.idx=-1; + var list=document.getElementById('upd-ig'); list.style.display='block'; + list.innerHTML=IGS.map(function(x,j){ + var st=j✓':j===ig.idx&&!ig.rb||(waiting&&j===0)?'⟳':'○'; + var t=j===ig.idx&&!ig.rb&&s&&s.stage!=='done'?' — '+(s.msg||'')+', '+(s.elapsed||0)+' с':''; + return '
  • '+st+' '+x[1]+t+'
  • '; }).join('')+(s&&s.stage==='done'&&!waiting?'
  • ✓ Шлюз установлен и работает
  • ':''); + var lw=document.getElementById('upd-ig-logw'), lg=document.getElementById('upd-ig-log'); + if(s&&s.log){ lw.style.display='block'; lg.textContent=s.log; lg.scrollTop=lg.scrollHeight; } + } + window.bjIgPhase=function(note){ + if(ig.on) return; ig.on=true; ig.t0=Date.now(); window.bjIgOv=true; finished=true; + show(); document.querySelector('#upd-modal h3').textContent='Обновление шлюза НРД (ИШ)'; + doneEl.style.display='none'; bar.style.background='var(--accent,#2b50d8)'; bar.style.width='5%'; + if(note) detail.textContent=note.charAt(0).toUpperCase()+note.slice(1)+'.'; + (function tick(){ + fetch('/admin/setup/update/progress',{cache:'no-store',credentials:'same-origin'}).then(function(r){ if(!r.ok) throw 0; return r.json(); }).then(function(p){ + var s=p.igate||{}, run=s.busy===true, since=(Date.now()-ig.t0)/1000; + var fresh=run||ig.seen||(s.elapsed!==undefined&&s.elapsed15*60){ finishIg('bj не начал обновление шлюза за 15 минут. Шлюз работает на прежней версии; обновить можно на странице НРД → «Перенастроить» → «Шлюз».',false); return; } + stageEl.textContent='Получен новый ИШ'+(p.igate_to?' '+p.igate_to:'')+' — bj начнёт обновление шлюза через несколько секунд.'; } + setTimeout(tick,2500); + }).catch(function(){ setTimeout(tick,3000); }); + })(); + }; + function finishIg(msg, ok){ + stageEl.innerHTML=ok?'✓ Готово':'✗ Обновление шлюза не прошло'; + detail.textContent=msg||''; bar.style.width='100%'; bar.style.background=ok?'var(--ok,#2e8b57)':'var(--err,#c0392b)'; + doneEl.style.display='block'; var b=doneEl.querySelector('button'); + if(ok){ var left=8, t=function(){ if(left<=0){ location.reload(); return; } b.textContent='Готово (страница обновится через '+left+' с)'; left--; setTimeout(t,1000); }; t(); } + else { document.getElementById('upd-ig-logw').open=true; b.textContent='Закрыть'; } + } + function igPending(p){ return !!(p&&(p.igate_to||(p.igate&&p.igate.busy))); } + // страница открыта, когда шлюз обновляется (или вот-вот начнёт после перезапуска bj) — то же окно + fetch('/admin/setup/update/progress',{cache:'no-store',credentials:'same-origin'}).then(function(r){ return r.ok?r.json():null; }).then(function(p){ + if(!p||p.active) return; + var s=p.igate||{}; + if(s.busy||(p.igate_to&&s.stage!=='error'&&p.up<12*60)) window.bjIgPhase(''); + }).catch(function(){}); function run(form){ var lastChange=Date.now(), lastKey=''; // сторож: если отчёт не меняется полторы минуты, перезагружаем страницу — не оставляем «зависшее» окно @@ -1087,7 +1146,9 @@ button.dq-link, .dq-link { background:none !important; border:0 !important; padd // сменилась или отчёт начался заново (новый процесс) — обновление завершено if(!lost && startVer!==null && (p.version!==startVer || (seq0>0 && p.seq===0 && !p.active))){ lost=true; } if(lost){ // bj вернулся после перезапуска - if(!target || p.version===target || p.version!==startVer){ finish('Готово: bj перезапущен, работает версия '+p.version+'.', true); detail.textContent=note ? note.charAt(0).toUpperCase()+note.slice(1)+'.' : ''; return; } + if(!target || p.version===target || p.version!==startVer){ + if(igPending(p)){ window.bjIgPhase('bj обновлён до '+p.version+'; теперь обновляется шлюз НРД'); return; } + finish('Готово: bj перезапущен, работает версия '+p.version+'.', true); detail.textContent=note ? note.charAt(0).toUpperCase()+note.slice(1)+'.' : ''; return; } } var fresh = p.seq!==seq0; // только отчёт этой попытки if(p.active){ seenActive=true; @@ -1099,6 +1160,7 @@ button.dq-link, .dq-link { background:none !important; border:0 !important; padd } else if(p.finished && fresh){ if(p.error){ finish('Обновление не выполнено', false); detail.textContent=p.error; return; } if(target && p.version!==target){ stageEl.textContent='Шаг 4 из 4: перезапуск bj'; bar.style.width='90%'; detail.textContent='Ждём, пока bj поднимется с новой версией…'; } + else if(igPending(p)){ window.bjIgPhase(p.result||''); return; } else { finish('Готово', true); detail.textContent=p.result||''; return; } } else if(postDone && !fresh && Date.now()-t0>8000){ finish('Готово', true); detail.textContent='Результат — в строке «Последняя проверка».'; return; diff --git a/internal/lkgateway/web/templates/part_igate.html b/internal/lkgateway/web/templates/part_igate.html index c633282..2db6a1c 100644 --- a/internal/lkgateway/web/templates/part_igate.html +++ b/internal/lkgateway/web/templates/part_igate.html @@ -354,7 +354,7 @@ 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 ovStart(){ if(window.bjIgPhase){ window.bjIgPhase(''); return; } 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';