diff --git a/deploy/linux/sbin/bj-igate-admin b/deploy/linux/sbin/bj-igate-admin index 3388c6d..94b3f24 100755 --- a/deploy/linux/sbin/bj-igate-admin +++ b/deploy/linux/sbin/bj-igate-admin @@ -89,15 +89,21 @@ case "$cmd" in 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 "установка ИШ уже идёт" + if pgrep -f "bj-igate-admin install-now" >/dev/null; then + # идёт — не мешать; но если ход не менялся 15 минут, установка зависла: снять её и начать заново + age=$(( $(date +%s) - $(stat -c %Y "$STATUS" 2>/dev/null || echo 0) )) + [ "$age" -gt 900 ] || err "установка ИШ уже идёт" + systemctl stop 'bj-igate-install-*' >/dev/null 2>&1; pkill -f "bj-igate-admin install-now" 2>/dev/null; sleep 2 + fi file=$(j file); ver=$(j version) [ "$(j bundled)" = 1 ] && [ -z "$file$ver" ] && { file=$(ls /var/lib/bj/igate-dist/igate-*-amd64.zip 2>/dev/null | sort -V | tail -1); [ -n "$file" ] || err "в образе нет дистрибутива ИШ"; BJ_EXTRA="{\"file\":\"$file\"}"; } [ -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 "не удалось запустить установку" + echo '{"stage":"queued","msg":"установка запущена"}' > "$STATUS"; chown bj:bj "$STATUS" 2>/dev/null + # не дольше 40 минут: что бы ни зависло, служба установки будет остановлена (ход покажет «прервалась») + systemd-run --unit="bj-igate-install-$(date +%s)" --collect -p RuntimeMaxSec=2400 /usr/local/sbin/bj-igate-admin install-now >/dev/null 2>&1 || err "не удалось запустить установку" ok "установка ИШ запущена" ;; install-now) @@ -121,7 +127,7 @@ PY { DEBIAN_FRONTEND=noninteractive apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends $need; } || die "не удалось установить пакеты:$need" fi - st package "дистрибутив ИШ" + st dist "дистрибутив ИШ" file=$(j file); ver=$(j version) if [ -z "$file" ]; then v=$(printf '%s' "$ver" | tr . -) @@ -160,7 +166,7 @@ PY runuser -u postgres -- psql -qtAc "select pg_reload_conf()" >/dev/null fi - st package "установка пакета ИШ" + st install "установка пакета ИШ" 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 @@ -216,7 +222,8 @@ EOF 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" + # -k: .NET-программа может не завершиться по SIGTERM — через 15 с принудительно (иначе ждали бы бесконечно) + timeout -k 15 150 bash -c "$(declare -f run_as_igate_keyring); SVC_USER=$SVC_USER HOME_DIR=$HOME_DIR run_as_igate_keyring 'timeout -k 15 90 $IGATE_DIR/igate-cli/igate-cli --data $HOME_DIR /dev/null 2>&1; true'" || true psqlq -c "select to_regclass('public.parameters') is not null" | grep -q t || die "ИШ не создал схему базы (igate-cli). Журнал: $LOG" fi # Общие параметры ИШ. Обязательные для работы без графики и для bj (движок при старте, Web API на diff --git a/deploy/release/igate-approved b/deploy/release/igate-approved new file mode 100644 index 0000000..0f09558 --- /dev/null +++ b/deploy/release/igate-approved @@ -0,0 +1 @@ +107.0.930 996bcf9f8f0001dc0e73b9582a5ebc0fa05bda264bc8e8b1785d069ed07f13c3 diff --git a/internal/lkgateway/igate.go b/internal/lkgateway/igate.go index 0379c7e..4e5c6e6 100644 --- a/internal/lkgateway/igate.go +++ b/internal/lkgateway/igate.go @@ -5,6 +5,8 @@ package lkgateway import ( "context" "encoding/json" + "errors" + "fmt" "io" "log" "net/http" @@ -153,13 +155,14 @@ func registerIgateAdmin(mux *http.ServeMux, rc *RuntimeConfig) { return } params["file"] = dst - } else if r.FormValue("bundled") == "1" { - if igateBundledVersion() == "" { - jsonOut(w, map[string]any{"ok": false, "error": "в образе нет дистрибутива ИШ — скачайте с сайта НРД или загрузите файл"}) + } else if r.FormValue("bundled") == "1" { // проверенный дистрибутив на сервере (образ или обновление bj) + file, _ := igateDistFile() + if file == "" { + jsonOut(w, map[string]any{"ok": false, "error": "дистрибутива ИШ на сервере нет — он приходит с обновлением bj"}) return } - params["bundled"] = "1" - } else if v := strings.TrimSpace(r.FormValue("version")); igateVersionRe.MatchString(v) { + params["file"] = file + } else if v := strings.TrimSpace(r.FormValue("version")); igateVersionRe.MatchString(v) && devMode(rc) { // с сайта НРД — только разработчикам params["version"] = v } else { jsonOut(w, map[string]any{"ok": false, "error": "загрузите архив ИШ или укажите версию вида 107.0.930"}) @@ -170,15 +173,22 @@ func registerIgateAdmin(mux *http.ServeMux, rc *RuntimeConfig) { 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 { + raw, err := os.ReadFile(igateInstallStatusPath) + out := map[string]any{} + if err != nil || json.Unmarshal(raw, &out) != 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) + 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) }) // канал контура в ИШ (адрес, отпечаток УЦ, папка обмена); ключ — мастером установки ключа mux.HandleFunc("/admin/setup/igate/contour", func(w http.ResponseWriter, r *http.Request) { @@ -361,13 +371,71 @@ func igateInstallStatus() igateInstallState { } func igateBundledVersion() string { + _, v := igateDistFile() + return v +} + +// igateDistFile — самый новый дистрибутив ИШ на сервере (файл и версия): с образа (igate-<версия>-amd64.zip) +// или из обновления bj (компонент igate-dist, версия — в списке установленных компонентов). Это версии, +// проверенные ZETIT; другой ИШ клиенту не ставится. +func igateDistFile() (string, string) { + best, file := "", "" m, _ := filepath.Glob("/var/lib/bj/igate-dist/igate-*-amd64.zip") - best := "" for _, f := range m { v := strings.ReplaceAll(strings.TrimSuffix(strings.TrimPrefix(filepath.Base(f), "igate-"), "-amd64.zip"), "-", ".") if igateVersionRe.MatchString(v) && (best == "" || release.IsNewer(v, best)) { - best = v + best, file = v, f + } + } + if c, ok := release.ComponentByName("igate-dist"); ok { + if inst, ok := release.LoadInstalled(release.InstalledFile)[c.Name]; ok && igateVersionRe.MatchString(inst.Version) { + if _, err := os.Stat(c.Path); err == nil && (best == "" || release.IsNewer(inst.Version, best)) { + best, file = inst.Version, c.Path + } + } + } + return file, best +} + +// igateInstallDist — поставить (или обновить) ИШ из самого нового дистрибутива на сервере. +func igateInstallDist(ctx context.Context) error { + file, _ := igateDistFile() + if file == "" { + return errors.New("дистрибутива ИШ на сервере нет — он приходит с обновлением bj") + } + extra, _ := json.Marshal(map[string]string{"file": file}) + res, err := runKeymediaOp(ctx, "igate-install", map[string]string{"extra": string(extra)}) + if err == nil { + if ok, _ := res["ok"].(bool); !ok { + err = fmt.Errorf("%v", res["error"]) + } + } + return err +} + +// igateAutoUpgrade — ИШ на сервере старее дистрибутива, пришедшего с обновлением bj: обновить (настройки, +// каналы и база ИШ сохраняются; прежняя версия копируется в /var/backups). Вызывается после обновления bj +// и при запуске bj-server (обновление ядра перезапускает его до того, как успел бы сработать первый вызов). +func igateAutoUpgrade(ctx context.Context, rc *RuntimeConfig) { + cur := ishInstalledVersion() + _, dist := igateDistFile() + if !igateInstalled() || cur == "" || dist == "" || !release.IsNewer(dist, cur) || igateInstallStatus().Busy { + return + } + log.Printf("lk-gateway: ИШ %s старее проверенного дистрибутива %s — обновляю", cur, dist) + if err := igateInstallDist(ctx); err != nil { + raiseSystemAlert(rc, "ИШ не обновлён до "+dist, "Пришёл дистрибутив ИШ "+dist+", но обновление не запустилось: "+err.Error()+". Повторить — мастер, шаг «Шлюз НРД».") + return + } + for ctx.Err() == nil { + time.Sleep(5 * time.Second) + if st := igateInstallStatus(); !st.Busy { + if st.Stage == "done" { + raiseSystemAlert(rc, "ИШ обновлён до "+dist, "Интеграционный шлюз НРД обновлён с "+cur+" до "+dist+" (версия, проверенная ZETIT). Настройки и каналы сохранены.") + } else { + raiseSystemAlert(rc, "ИШ не обновлён до "+dist, "Обновление ИШ с "+cur+" до "+dist+" не прошло: "+st.Msg+". Прежняя версия сохранена в /var/backups; повторить — мастер, шаг «Шлюз НРД».") + } + return } } - return best } diff --git a/internal/lkgateway/prepare.go b/internal/lkgateway/prepare.go index 70707a0..ad5933d 100644 --- a/internal/lkgateway/prepare.go +++ b/internal/lkgateway/prepare.go @@ -7,7 +7,6 @@ package lkgateway import ( "context" - "fmt" "net/http" "os" "os/exec" @@ -198,13 +197,7 @@ func prepareIgate(ctx context.Context) { 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 { + if err := igateInstallDist(ctx); err != nil { prepareRun.Lock() prepareRun.IgateErr = err.Error() prepareRun.Unlock() @@ -228,7 +221,7 @@ func prepareIgateItem() PrepareItem { prepareRun.Unlock() switch { case st.Busy: - it.State, it.Detail = "run", "устанавливается: "+st.Msg+" (2–5 минут)" + it.State, it.Detail = "run", "устанавливается: "+st.Msg+" (вся установка — 2–5 минут)" case igateInstalled(): it.State, it.Detail = "ok", "установлен"+pickStr(prefixed(", версия ", ishInstalledVersion()), "") case st.Stage == "error": diff --git a/internal/lkgateway/server.go b/internal/lkgateway/server.go index c366cb4..f39cb21 100644 --- a/internal/lkgateway/server.go +++ b/internal/lkgateway/server.go @@ -366,6 +366,13 @@ func (s *Server) Run(ctx context.Context) error { // Лицензия: ежесуточное продление ключа на сервере лицензий lic.zetit.ru. go superviseLoop(ctx, "license", s.rc, s.licenseLoop) go publicIPLoop(ctx) // внешний адрес сервера — для письма в ARQA (у сервера лицензий, раз в час) + go func() { // ИШ старее дистрибутива из обновления bj (ядро перезапускалось при обновлении) — обновить + select { + case <-ctx.Done(): + case <-time.After(2 * time.Minute): + igateAutoUpgrade(ctx, s.rc) + } + }() // Авто-обновление сертификатов УЦ раз в сутки (если оператор включил). stopCACerts := StartCACertsAutoUpdater(s.rc) diff --git a/internal/lkgateway/updater.go b/internal/lkgateway/updater.go index 8b02808..7aa6ccc 100644 --- a/internal/lkgateway/updater.go +++ b/internal/lkgateway/updater.go @@ -237,6 +237,9 @@ func (u *Updater) ApplyUpdate(ctx context.Context, names []string) (string, erro progressStage(1, "Проверка выпуска на сервере лицензий") msg, err := u.applyUpdate(ctx, names) progressEnd(msg, err) + if err == nil { // с обновлением мог прийти новый проверенный дистрибутив ИШ — обновить шлюз + go igateAutoUpgrade(context.Background(), u.rc) + } return msg, err } @@ -443,7 +446,7 @@ func (u *Updater) ensureSplitModules(ctx context.Context) (string, error) { if len(names) == 0 { return "", nil } - cctx, cancel := context.WithTimeout(ctx, 10*time.Minute) + cctx, cancel := context.WithTimeout(ctx, 30*time.Minute) // с дистрибутивом ИШ (160 МБ) скачивание дольше defer cancel() msg, err := u.ApplyUpdate(cctx, names) if err != nil { diff --git a/internal/lkgateway/web/templates/part_igate.html b/internal/lkgateway/web/templates/part_igate.html index 6afa780..e0abaa9 100644 --- a/internal/lkgateway/web/templates/part_igate.html +++ b/internal/lkgateway/web/templates/part_igate.html @@ -8,6 +8,19 @@ #ig-ct td{vertical-align:top;padding:6px 8px 6px 0} #ig-box .btn.sm{padding:4px 10px;font-size:12.5px} #ig-box .btn.ghost{background:var(--card);color:var(--text);border:1px solid var(--border-strong,#c9cdd8)} +/* ход установки ИШ: этапы по порядку, у текущего — сколько идёт и сколько обычно */ +.ig-steps{list-style:none;margin:10px 0 0;padding:10px 14px;border:1px solid var(--border);border-radius:10px;display:grid;gap:2px} +.ig-steps li{display:flex;gap:10px;align-items:flex-start;padding:4px 0;font-size:13.5px;color:var(--muted)} +.ig-steps li.done{color:var(--text-2)} .ig-steps li.cur{color:var(--text);font-weight:500} .ig-steps li.err{color:var(--err)} +.ig-ic{flex:none;position:relative;width:16px;height:16px;border-radius:50%;margin-top:2px;box-sizing:border-box;border:1.5px solid var(--border-strong)} +.ig-steps li.done .ig-ic{background:var(--ok);border-color:var(--ok)} +.ig-steps li.done .ig-ic::after{content:"";position:absolute;left:50%;top:45%;width:3.5px;height:7px;border:solid #fff;border-width:0 1.6px 1.6px 0;transform:translate(-50%,-50%) rotate(45deg)} +.ig-steps li.cur .ig-ic{border:2px solid var(--accent-weak);border-top-color:var(--accent);animation:ig-spin .8s linear infinite} +.ig-steps li.err .ig-ic{background:var(--err);border-color:var(--err)} +.ig-t small{display:block;font-weight:400;color:var(--muted);font-size:12px} +.ig-t small.slow{color:var(--warn)} +@keyframes ig-spin{to{transform:rotate(360deg)}} +@media (prefers-reduced-motion:reduce){.ig-steps li.cur .ig-ic{animation:none;border-color:var(--accent)}}
Версия ИШ — та, что проверена ZETIT вместе с bj; новые версии приходят с обновлением bj. bj ставит шлюз сам при подготовке.
+