ИШ обновляется через сервер лицензий вместе с bj; наглядный ход установки ИШ

- Дистрибутив ИШ — компонент выпуска igate-dist (версия = версия ИШ). В выпуск
  попадает только одобренный архив: deploy/release/igate-approved (версия и
  sha256), сборка сверяет контрольную сумму. Клиент получает его обновлением;
  bj-server после обновления (и при запуске) сам обновляет ИШ, если на сервере
  он старее; итог — событие в «Новостях». Настройки, каналы и база ИШ
  сохраняются, прежняя версия — в /var/backups.
- Установка «с сервера» берёт самый новый проверенный дистрибутив: с образа или из
  обновления. С сайта НРД — только в режиме разработки.
- Большие файлы: одна прежняя копия вместо пяти; предел скачивания — по размеру.
- Ход установки ИШ — этапами: пройденные, текущий («идёт 0:41 · обычно до 1:30»,
  дольше обычного — предупреждение), журнал под раскрытием; кнопка на время
  установки выключена.
- Установщик: уникальные этапы; igate-cli и сессия ключницы с принудительным
  завершением (timeout -k); зависшая установка (ход не менялся 15 минут)
  снимается повторным запуском; служба установки — не дольше 40 минут.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
zuevav
2026-10-06 17:42:54 +03:00
parent 920c030ca5
commit 648a5ae05f
11 changed files with 196 additions and 42 deletions
+13 -6
View File
@@ -89,15 +89,21 @@ case "$cmd" in
echo "{\"ok\":true,\"installed\":$inst,\"version\":$(q "$(igate_version)"),\"service\":$(q "$svc"),\"engine\":$(q "$st"),\"channels\":${chans:-[]}}" ;; echo "{\"ok\":true,\"installed\":$inst,\"version\":$(q "$(igate_version)"),\"service\":$(q "$svc"),\"engine\":$(q "$st"),\"channels\":${chans:-[]}}" ;;
install) 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) 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\"}"; } [ "$(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 "не указан дистрибутив ИШ (файл или версия)" [ -n "$file$ver" ] || err "не указан дистрибутив ИШ (файл или версия)"
[ -z "$file" ] || [ -f "$file" ] || err "файл дистрибутива не найден: $file" [ -z "$file" ] || [ -f "$file" ] || err "файл дистрибутива не найден: $file"
[[ -z "$ver" || "$ver" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || err "версия ИШ — вида 107.0.930" [[ -z "$ver" || "$ver" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || err "версия ИШ — вида 107.0.930"
(umask 077; printf '%s' "${BJ_EXTRA:-}" > "$PARAMS") (umask 077; printf '%s' "${BJ_EXTRA:-}" > "$PARAMS")
echo '{"stage":"start","msg":"установка запущена"}' > "$STATUS"; chown bj:bj "$STATUS" 2>/dev/null echo '{"stage":"queued","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 "не удалось запустить установку" # не дольше 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 "установка ИШ запущена" ;; ok "установка ИШ запущена" ;;
install-now) 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" { DEBIAN_FRONTEND=noninteractive apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends $need; } || die "не удалось установить пакеты:$need"
fi fi
st package "дистрибутив ИШ" st dist "дистрибутив ИШ"
file=$(j file); ver=$(j version) file=$(j file); ver=$(j version)
if [ -z "$file" ]; then if [ -z "$file" ]; then
v=$(printf '%s' "$ver" | tr . -) v=$(printf '%s' "$ver" | tr . -)
@@ -160,7 +166,7 @@ PY
runuser -u postgres -- psql -qtAc "select pg_reload_conf()" >/dev/null runuser -u postgres -- psql -qtAc "select pg_reload_conf()" >/dev/null
fi fi
st package "установка пакета ИШ" st install "установка пакета ИШ"
systemctl stop igate >/dev/null 2>&1 || true 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.* 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 if [ -d "$IGATE_DIR" ]; then
@@ -216,7 +222,8 @@ EOF
st schema "база ИШ: создание схемы" st schema "база ИШ: создание схемы"
if ! psqlq -c "select to_regclass('public.parameters') is not null" | grep -q t; then 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 >/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 >/dev/null 2>&1; true'" || true
psqlq -c "select to_regclass('public.parameters') is not null" | grep -q t || die "ИШ не создал схему базы (igate-cli). Журнал: $LOG" psqlq -c "select to_regclass('public.parameters') is not null" | grep -q t || die "ИШ не создал схему базы (igate-cli). Журнал: $LOG"
fi fi
# Общие параметры ИШ. Обязательные для работы без графики и для bj (движок при старте, Web API на # Общие параметры ИШ. Обязательные для работы без графики и для bj (движок при старте, Web API на
+1
View File
@@ -0,0 +1 @@
107.0.930 996bcf9f8f0001dc0e73b9582a5ebc0fa05bda264bc8e8b1785d069ed07f13c3
+81 -13
View File
@@ -5,6 +5,8 @@ package lkgateway
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt"
"io" "io"
"log" "log"
"net/http" "net/http"
@@ -153,13 +155,14 @@ func registerIgateAdmin(mux *http.ServeMux, rc *RuntimeConfig) {
return return
} }
params["file"] = dst params["file"] = dst
} else if r.FormValue("bundled") == "1" { } else if r.FormValue("bundled") == "1" { // проверенный дистрибутив на сервере (образ или обновление bj)
if igateBundledVersion() == "" { file, _ := igateDistFile()
jsonOut(w, map[string]any{"ok": false, "error": "в образе нет дистрибутива ИШ — скачайте с сайта НРД или загрузите файл"}) if file == "" {
jsonOut(w, map[string]any{"ok": false, "error": "дистрибутива ИШ на сервере нет — он приходит с обновлением bj"})
return return
} }
params["bundled"] = "1" params["file"] = file
} else if v := strings.TrimSpace(r.FormValue("version")); igateVersionRe.MatchString(v) { } else if v := strings.TrimSpace(r.FormValue("version")); igateVersionRe.MatchString(v) && devMode(rc) { // с сайта НРД — только разработчикам
params["version"] = v params["version"] = v
} else { } else {
jsonOut(w, map[string]any{"ok": false, "error": "загрузите архив ИШ или укажите версию вида 107.0.930"}) jsonOut(w, map[string]any{"ok": false, "error": "загрузите архив ИШ или укажите версию вида 107.0.930"})
@@ -170,15 +173,22 @@ func registerIgateAdmin(mux *http.ServeMux, rc *RuntimeConfig) {
defer cancel() defer cancel()
jsonOut(w, opResult(runKeymediaOp(ctx, "igate-install", map[string]string{"extra": string(extra)}))) 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) { 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") raw, err := os.ReadFile(igateInstallStatusPath)
if err != nil { out := map[string]any{}
if err != nil || json.Unmarshal(raw, &out) != nil {
jsonOut(w, map[string]any{}) jsonOut(w, map[string]any{})
return return
} }
w.Header().Set("Content-Type", "application/json; charset=utf-8") if fi, err := os.Stat(igateInstallStatusPath); err == nil {
w.Header().Set("Cache-Control", "no-store") out["elapsed"] = int(time.Since(fi.ModTime()).Seconds())
_, _ = w.Write(raw) }
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) { mux.HandleFunc("/admin/setup/igate/contour", func(w http.ResponseWriter, r *http.Request) {
@@ -361,13 +371,71 @@ func igateInstallStatus() igateInstallState {
} }
func igateBundledVersion() string { 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") m, _ := filepath.Glob("/var/lib/bj/igate-dist/igate-*-amd64.zip")
best := ""
for _, f := range m { for _, f := range m {
v := strings.ReplaceAll(strings.TrimSuffix(strings.TrimPrefix(filepath.Base(f), "igate-"), "-amd64.zip"), "-", ".") v := strings.ReplaceAll(strings.TrimSuffix(strings.TrimPrefix(filepath.Base(f), "igate-"), "-amd64.zip"), "-", ".")
if igateVersionRe.MatchString(v) && (best == "" || release.IsNewer(v, best)) { 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
} }
+2 -9
View File
@@ -7,7 +7,6 @@ package lkgateway
import ( import (
"context" "context"
"fmt"
"net/http" "net/http"
"os" "os"
"os/exec" "os/exec"
@@ -198,13 +197,7 @@ func prepareIgate(ctx context.Context) {
return return
} }
if st := igateInstallStatus(); !st.Busy { if st := igateInstallStatus(); !st.Busy {
res, err := runKeymediaOp(ctx, "igate-install", map[string]string{"extra": `{"bundled":"1"}`}) if err := igateInstallDist(ctx); err != nil {
if err == nil {
if ok, _ := res["ok"].(bool); !ok {
err = fmt.Errorf("%v", res["error"])
}
}
if err != nil {
prepareRun.Lock() prepareRun.Lock()
prepareRun.IgateErr = err.Error() prepareRun.IgateErr = err.Error()
prepareRun.Unlock() prepareRun.Unlock()
@@ -228,7 +221,7 @@ func prepareIgateItem() PrepareItem {
prepareRun.Unlock() prepareRun.Unlock()
switch { switch {
case st.Busy: case st.Busy:
it.State, it.Detail = "run", "устанавливается: "+st.Msg+" (2–5 минут)" it.State, it.Detail = "run", "устанавливается: "+st.Msg+" (вся установка — 2–5 минут)"
case igateInstalled(): case igateInstalled():
it.State, it.Detail = "ok", "установлен"+pickStr(prefixed(", версия ", ishInstalledVersion()), "") it.State, it.Detail = "ok", "установлен"+pickStr(prefixed(", версия ", ishInstalledVersion()), "")
case st.Stage == "error": case st.Stage == "error":
+7
View File
@@ -366,6 +366,13 @@ func (s *Server) Run(ctx context.Context) error {
// Лицензия: ежесуточное продление ключа на сервере лицензий lic.zetit.ru. // Лицензия: ежесуточное продление ключа на сервере лицензий lic.zetit.ru.
go superviseLoop(ctx, "license", s.rc, s.licenseLoop) go superviseLoop(ctx, "license", s.rc, s.licenseLoop)
go publicIPLoop(ctx) // внешний адрес сервера — для письма в ARQA (у сервера лицензий, раз в час) 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) stopCACerts := StartCACertsAutoUpdater(s.rc)
+4 -1
View File
@@ -237,6 +237,9 @@ func (u *Updater) ApplyUpdate(ctx context.Context, names []string) (string, erro
progressStage(1, "Проверка выпуска на сервере лицензий") progressStage(1, "Проверка выпуска на сервере лицензий")
msg, err := u.applyUpdate(ctx, names) msg, err := u.applyUpdate(ctx, names)
progressEnd(msg, err) progressEnd(msg, err)
if err == nil { // с обновлением мог прийти новый проверенный дистрибутив ИШ — обновить шлюз
go igateAutoUpgrade(context.Background(), u.rc)
}
return msg, err return msg, err
} }
@@ -443,7 +446,7 @@ func (u *Updater) ensureSplitModules(ctx context.Context) (string, error) {
if len(names) == 0 { if len(names) == 0 {
return "", nil return "", nil
} }
cctx, cancel := context.WithTimeout(ctx, 10*time.Minute) cctx, cancel := context.WithTimeout(ctx, 30*time.Minute) // с дистрибутивом ИШ (160 МБ) скачивание дольше
defer cancel() defer cancel()
msg, err := u.ApplyUpdate(cctx, names) msg, err := u.ApplyUpdate(cctx, names)
if err != nil { if err != nil {
@@ -8,6 +8,19 @@
#ig-ct td{vertical-align:top;padding:6px 8px 6px 0} #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.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-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)}}
</style> </style>
<div id="ig-box"> <div id="ig-box">
<div class="card" style="margin:10px 0"> <div class="card" style="margin:10px 0">
@@ -15,8 +28,10 @@
<div id="ig-head" class="muted">Проверяю…</div> <div id="ig-head" class="muted">Проверяю…</div>
<p class="muted" style="font-size:13px;margin:6px 0 0">Версия ИШ — та, что проверена ZETIT вместе с bj; новые версии приходят с обновлением bj. bj ставит шлюз сам при подготовке.</p> <p class="muted" style="font-size:13px;margin:6px 0 0">Версия ИШ — та, что проверена ZETIT вместе с bj; новые версии приходят с обновлением bj. bj ставит шлюз сам при подготовке.</p>
<div id="ig-reinst" style="display:none;margin-top:10px"><button type="button" class="btn" id="ig-bnd">Установить ИШ</button> <span class="muted" id="ig-bnd-v" style="font-size:12.5px"></span></div> <div id="ig-reinst" style="display:none;margin-top:10px"><button type="button" class="btn" id="ig-bnd">Установить ИШ</button> <span class="muted" id="ig-bnd-v" style="font-size:12.5px"></span></div>
<ol class="ig-steps" id="ig-steps" hidden></ol>
<div id="ig-out" style="margin-top:8px;font-size:13px"></div> <div id="ig-out" style="margin-top:8px;font-size:13px"></div>
<pre id="ig-log" style="display:none;max-height:180px;overflow:auto;font-size:11px;margin-top:6px"></pre> <details id="ig-logbox" hidden style="margin-top:6px;font-size:12.5px"><summary style="cursor:pointer;color:var(--accent)">Журнал установки</summary>
<pre id="ig-log" style="max-height:220px;overflow:auto;font-size:11px;margin-top:6px"></pre></details>
{{if devOn}}<details style="margin-top:8px"><summary style="cursor:pointer;color:var(--accent);font-size:13px">Для разработчиков: другая версия ИШ</summary> {{if devOn}}<details style="margin-top:8px"><summary style="cursor:pointer;color:var(--accent);font-size:13px">Для разработчиков: другая версия ИШ</summary>
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:end;margin-top:8px"> <div style="display:flex;gap:8px;flex-wrap:wrap;align-items:end;margin-top:8px">
<label class="f">Версия на сайте НРД<input id="ig-ver" value="107.0.930" style="width:140px"></label> <label class="f">Версия на сайте НРД<input id="ig-ver" value="107.0.930" style="width:140px"></label>
@@ -104,23 +119,51 @@
post('/admin/setup/igate/switch',p).then(function(d){ cout.innerHTML=d.ok?ok(d.output):bad(d.error); if(d.ok) setTimeout(function(){location.reload()},15000); }); post('/admin/setup/igate/switch',p).then(function(d){ cout.innerHTML=d.ok?ok(d.output):bad(d.error); if(d.ok) setTimeout(function(){location.reload()},15000); });
}; };
document.getElementById('ig-crypto').onchange=load; document.getElementById('ig-crypto').onchange=load;
function poll(){ var t0=Date.now(), iv=setInterval(function(){ // этапы установщика (bj-igate-admin) по порядку и сколько каждый обычно длится, секунд
var STAGES=[['queued','Запуск установки',15],['deps','Системные пакеты для работы без графики',120],['dist','Дистрибутив ИШ',30],
['user','Пользователь igate и хранилище паролей',15],['db','База ИШ в PostgreSQL',20],['install','Установка пакета ИШ',90],
['config','Настройка для работы без графики',15],['schema','Создание схемы базы ИШ',90],['start','Запуск ИШ и движка',120]];
var stepsEl=document.getElementById('ig-steps'), logbox=document.getElementById('ig-logbox'), cur={key:'',since:0,s:null}, tick=0;
function stageKey(s){ // прежний установщик называл два этапа «package», а первый и последний — «start»
if(s.stage==='package') return /дистрибутив/.test(s.msg||'')?'dist':'install';
if(s.stage==='start' && /запущена/.test(s.msg||'')) return 'queued';
return s.stage; }
function mmss(n){ n=Math.max(0,Math.round(n)); return Math.floor(n/60)+':'+('0'+n%60).slice(-2); }
function draw(){
var s=cur.s; if(!s) return;
var k=stageKey(s), idx=-1; STAGES.forEach(function(x,i){ if(x[0]===k) idx=i; });
if(s.stage==='done') idx=STAGES.length;
var el=(Date.now()-cur.since)/1000;
stepsEl.hidden=false;
stepsEl.innerHTML=STAGES.map(function(x,i){
var cls=i<idx?'done':i===idx?(s.stage==='error'?'err':'cur'):'', note='';
if(i===idx && s.stage!=='error'){ var slow=el>x[2]*2;
note='<small'+(slow?' class="slow"':'')+'>идёт '+mmss(el)+' · обычно до '+mmss(x[2])+(slow?' — дольше обычного; установка работает, пока счётчик идёт; зависшая снимается сама через 40 минут':'')+'</small>'; }
return '<li class="'+cls+'"><span class="ig-ic"></span><span class="ig-t">'+esc(x[1])+note+'</span></li>'; }).join('');
}
function poll(){ var t0=Date.now(), btn=document.getElementById('ig-bnd');
btn.disabled=true; // пока идёт установка — второй раз не нажать
clearInterval(tick); tick=setInterval(draw,1000);
var iv=setInterval(function(){
fetch('/admin/setup/igate/install-status',{cache:'no-store'}).then(function(r){return r.json()}).then(function(s){ fetch('/admin/setup/igate/install-status',{cache:'no-store'}).then(function(r){return r.json()}).then(function(s){
if(!s.stage) return; out.textContent='Установка: '+s.msg; if(s.log){ lg.style.display='block'; lg.textContent=s.log; lg.scrollTop=lg.scrollHeight; } if(!s.stage) return;
if(s.stage==='done'){ clearInterval(iv); out.innerHTML=ok(s.msg); load(); } if(stageKey(s)!==cur.key){ cur.key=stageKey(s); cur.since=Date.now()-(s.elapsed||0)*1000; }
if(s.stage==='error'){ clearInterval(iv); out.innerHTML=bad(s.msg); } cur.s=s; draw(); out.textContent='';
if(s.log){ logbox.hidden=false; lg.textContent=s.log; lg.scrollTop=lg.scrollHeight; }
if(s.stage==='done'){ clearInterval(iv); clearInterval(tick); btn.disabled=false; out.innerHTML=ok(s.msg); load(); }
if(s.stage==='error'){ clearInterval(iv); clearInterval(tick); btn.disabled=false; out.innerHTML=bad(s.msg)+' — можно нажать «Установить ИШ заново»'; logbox.open=true; load(); }
}).catch(function(){}); }).catch(function(){});
if(Date.now()-t0>1200000) clearInterval(iv); },3000); } if(Date.now()-t0>2700000){ clearInterval(iv); clearInterval(tick); } },3000); }
document.getElementById('ig-bnd').onclick=function(){ var p=new URLSearchParams(); p.set('bundled','1'); 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(); }); }; out.textContent='Запускаю…'; cur={key:'',since:0,s:null}; post('/admin/setup/igate/install',p).then(function(d){ if(!d.ok){out.innerHTML=bad(d.error);return} out.textContent=d.output; poll(); }); };
if(document.getElementById('ig-dl')) 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(); }); }; out.textContent='Запускаю…'; cur={key:'',since:0,s:null}; post('/admin/setup/igate/install',p).then(function(d){ if(!d.ok){out.innerHTML=bad(d.error);return} out.textContent=d.output; poll(); }); };
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; } 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+'…'; 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(); }); }; 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(); 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(){}); 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') poll(); }).catch(function(){});
})(); })();
</script> </script>
{{end}} {{end}}
+8 -1
View File
@@ -88,7 +88,14 @@ func (c *Client) DownloadArtifact(ctx context.Context, a Artifact, destDir strin
return "", err return "", err
} }
c.authorize(req) c.authorize(req)
resp, err := c.HTTP.Do(req) hc := c.HTTP
if hc.Timeout > 0 && a.Size > 16<<20 {
// большой файл (дистрибутив ИШ — 160 МБ): общий предел запроса — по размеру, из расчёта 256 КБ/с
cp := *hc
cp.Timeout = hc.Timeout + time.Duration(a.Size/(256<<10))*time.Second
hc = &cp
}
resp, err := hc.Do(req)
if err != nil { if err != nil {
return "", fmt.Errorf("release: скачивание %s: %w", a.Name, err) return "", fmt.Errorf("release: скачивание %s: %w", a.Name, err)
} }
+6
View File
@@ -26,6 +26,8 @@ type Component struct {
FromCore bool FromCore bool
// Feature — признак лицензии, без которого сервер обновлений не отдаёт файл компонента. // Feature — признак лицензии, без которого сервер обновлений не отдаёт файл компонента.
Feature string Feature string
// Big — большой файл (дистрибутив): при замене хранится одна прежняя копия, а не пять.
Big bool
} }
// Components — все компоненты bj. Порядок — порядок показа и установки (ядро перезапускается последним). // Components — все компоненты bj. Порядок — порядок показа и установки (ядро перезапускается последним).
@@ -42,6 +44,10 @@ var Components = []Component{
{Name: "bj-lk", Title: "Личный кабинет клиента", Path: "/opt/bj/bj-lk", Unit: "bj-lk", Exec: true, Feature: "web-cabinet"}, {Name: "bj-lk", Title: "Личный кабинет клиента", Path: "/opt/bj/bj-lk", Unit: "bj-lk", Exec: true, Feature: "web-cabinet"},
{Name: "bj-npr", Title: "Риск-монитор НПР / УДС", Path: "/opt/bj/bj-npr", Unit: "bj-npr", Exec: true, FromCore: true, Feature: "npr"}, {Name: "bj-npr", Title: "Риск-монитор НПР / УДС", Path: "/opt/bj/bj-npr", Unit: "bj-npr", Exec: true, FromCore: true, Feature: "npr"},
{Name: "bj-quikexport", Title: "Экспорт QUIK", Path: "/opt/bj/bj-quikexport", Unit: "bj-quikexport", Exec: true, FromCore: true, Feature: "quik-export"}, {Name: "bj-quikexport", Title: "Экспорт QUIK", Path: "/opt/bj/bj-quikexport", Unit: "bj-quikexport", Exec: true, FromCore: true, Feature: "quik-export"},
// Дистрибутив интеграционного шлюза НРД — версия, проверенная ZETIT вместе с bj (версия компонента =
// версия ИШ). Обновление кладёт файл; bj-server сам переставляет ИШ, если на сервере он старее
// (настройки и каналы сохраняются). С сайта НРД ИШ у клиента не качается.
{Name: "igate-dist", Title: "Интеграционный шлюз НРД (дистрибутив)", Path: "/var/lib/bj/igate-dist/igate-dist.zip", Core: true, Big: true, Feature: "updates"},
} }
// ComponentByName — компонент по имени артефакта. // ComponentByName — компонент по имени артефакта.
+8 -3
View File
@@ -134,7 +134,7 @@ func InstallStaged(staging string, opt InstallOptions) (res InstallResult) {
// с образа кладёт unit-файлы всех служб сразу (выключенными), а файл модуля приезжает обновлением. // с образа кладёт unit-файлы всех служб сразу (выключенными), а файл модуля приезжает обновлением.
_, statErr := os.Stat(j.c.PathAt(opt.Root)) _, statErr := os.Stat(j.c.PathAt(opt.Root))
fresh := statErr != nil fresh := statErr != nil
if err := installFile(j.file, j.c.PathAt(opt.Root), j.c.Exec, ts); err != nil { if err := installFile(j.file, j.c.PathAt(opt.Root), j.c.Exec, ts, j.c.Big); err != nil {
return fail("установка %s: %v", j.c.Name, err) return fail("установка %s: %v", j.c.Name, err)
} }
installed[j.c.Name] = Installed{Version: j.a.Version, SHA256: j.a.SHA256, InstalledAt: time.Now().UTC()} installed[j.c.Name] = Installed{Version: j.a.Version, SHA256: j.a.SHA256, InstalledAt: time.Now().UTC()}
@@ -248,15 +248,20 @@ func readStagedJSON(dir, name string, v any) error {
} }
// installFile — копия прежнего файла, запись нового рядом и атомарная замена. // installFile — копия прежнего файла, запись нового рядом и атомарная замена.
func installFile(src, dst string, execBit bool, ts string) error { func installFile(src, dst string, execBit bool, ts string, big bool) error {
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil { if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
return err return err
} }
if _, err := os.Stat(dst); err == nil { if _, err := os.Stat(dst); err == nil {
keep := 5
if big { // дистрибутив в сотни мегабайт — одна прежняя копия
keep = 1
}
pruneBackups(dst, keep-1)
if err := copyFileMode(dst, dst+".bak-"+ts, 0o755); err != nil { if err := copyFileMode(dst, dst+".bak-"+ts, 0o755); err != nil {
return fmt.Errorf("копия прежнего файла: %w", err) return fmt.Errorf("копия прежнего файла: %w", err)
} }
pruneBackups(dst, 5) pruneBackups(dst, keep)
} }
mode := os.FileMode(0o644) mode := os.FileMode(0o644)
if execBit { if execBit {
+14
View File
@@ -96,6 +96,20 @@ if [ -f "$JAR" ]; then
cver crypto-service.jar CRYPTO_VERSION "$(sha256sum "$JAR" | cut -d' ' -f1)" > "$DIST/crypto-service.jar.version" cver crypto-service.jar CRYPTO_VERSION "$(sha256sum "$JAR" | cut -d' ' -f1)" > "$DIST/crypto-service.jar.version"
fi fi
echo " криптосервис: $(cat "$DIST/crypto-service.jar.version" 2>/dev/null || echo нет)" echo " криптосервис: $(cat "$DIST/crypto-service.jar.version" 2>/dev/null || echo нет)"
# Дистрибутив ИШ — только версия, проверенная ZETIT вместе с bj: deploy/release/igate-approved («версия sha256»).
# Архив НРД — из IGATE_ZIP (по умолчанию ~/ish-upgrade/igate-<версия через дефис>-amd64.zip); контрольная
# сумма должна совпасть с одобренной. Версия компонента = версия ИШ: клиенты получают его с обновлением,
# bj-server сам обновляет ИШ, если на сервере он старее.
IGA="$ROOT/deploy/release/igate-approved"
if [ -f "$IGA" ]; then
read -r IG_VER IG_SHA < "$IGA"
IG_ZIP=${IGATE_ZIP:-$HOME/ish-upgrade/igate-$(echo "$IG_VER" | tr . -)-amd64.zip}
[ -f "$IG_ZIP" ] || { echo "нет дистрибутива ИШ $IG_VER: $IG_ZIP (IGATE_ZIP=…)"; exit 1; }
[ "$(sha256sum "$IG_ZIP" | cut -d' ' -f1)" = "$IG_SHA" ] || { echo "дистрибутив ИШ $IG_ZIP не совпадает с одобренным (deploy/release/igate-approved)"; exit 1; }
cp "$IG_ZIP" "$DIST/igate-dist"
echo "$IG_VER" > "$DIST/igate-dist.version"
echo " ИШ: $IG_VER (одобренный дистрибутив)"
fi
go run ./cmd/bj-release build -dir "$DIST" -version "$VERSION" -channel "$CHANNEL" -key "$KEY" -keyid release -out "$DIST/manifest.json" go run ./cmd/bj-release build -dir "$DIST" -version "$VERSION" -channel "$CHANNEL" -key "$KEY" -keyid release -out "$DIST/manifest.json"
# версии и отпечатки этого релиза — в репозиторий (закоммитить вместе с релизом) # версии и отпечатки этого релиза — в репозиторий (закоммитить вместе с релизом)
mkdir -p "$(dirname "$CV_STATE")" && mv "$CV_NEW" "$CV_STATE" mkdir -p "$(dirname "$CV_STATE")" && mv "$CV_NEW" "$CV_STATE"