1d8bb0c032
Поле пути в мастере и в «Таблицах обмена» принимает строку подключения из настройки Fancy: «сервер:путь», «сервер/порт:путь», «\\сервер\D:\путь», «\\сервер\D$\путь», «inet://сервер:порт/путь» — адрес и порт bj берёт из неё (в мастере поля заполняются сразу при вставке). Сетевая папка «\\сервер\папка\…» — ошибка с подсказкой: Firebird открывает базу только на своём диске, адрес — компьютер с сервером Firebird. Проверено мастером на копии базы: пустой адрес + «127.0.0.1/3050:путь» — подготовка и BJ_PING. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
536 lines
24 KiB
Go
536 lines
24 KiB
Go
package lkgateway
|
||
|
||
// Интеграционный шлюз НРД (ИШ, служба igate) на этом сервере: состояние для мастера (глава «НРД»).
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"io"
|
||
"log"
|
||
"net/http"
|
||
"os"
|
||
"os/exec"
|
||
"path/filepath"
|
||
"regexp"
|
||
"strings"
|
||
"time"
|
||
|
||
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/release"
|
||
)
|
||
|
||
const igateDefaultURL = "http://localhost:8090"
|
||
|
||
// igateInstalled — пакет ИШ стоит на этом сервере.
|
||
func igateInstalled() bool {
|
||
_, err := os.Stat("/opt/nsd/igate")
|
||
return err == nil
|
||
}
|
||
|
||
// igateState — отвечает ли ИШ и в каком состоянии его движок.
|
||
func igateState(ctx context.Context, base string) (bool, string) {
|
||
base = strings.TrimRight(pickStr(strings.TrimSpace(base), igateDefaultURL), "/")
|
||
cctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||
defer cancel()
|
||
req, _ := http.NewRequestWithContext(cctx, http.MethodGet, base+"/api/admin/engine/state", nil)
|
||
resp, err := http.DefaultClient.Do(req)
|
||
if err != nil {
|
||
if !igateInstalled() {
|
||
return false, "интеграционный шлюз на этом сервере не установлен"
|
||
}
|
||
out, _ := exec.CommandContext(ctx, "systemctl", "is-active", "igate").Output()
|
||
return false, "шлюз установлен, но не отвечает по адресу " + base + " (служба igate: " + pickStr(strings.TrimSpace(string(out)), "неизвестно") + ")"
|
||
}
|
||
defer resp.Body.Close()
|
||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 256))
|
||
st := strings.Trim(strings.TrimSpace(string(body)), `"`)
|
||
if resp.StatusCode != http.StatusOK {
|
||
return false, "шлюз отвечает с ошибкой HTTP " + resp.Status
|
||
}
|
||
if st != "Running" {
|
||
return false, "шлюз отвечает, но его движок в состоянии «" + st + "» — нужен Running"
|
||
}
|
||
return true, "шлюз отвечает по адресу " + base + ", движок работает"
|
||
}
|
||
|
||
// igateChannelState — загрузил ли ИШ канал с этим составным кодом (<канал контура><код депонента>) и включён ли он.
|
||
// Движок может быть Running и без единого канала: ИШ молча выкидывает канал, который не смог прочитать из базы
|
||
// (06.10.2026 на .29: пустые строки вместо NULL в необязательных параметрах), — и тогда любой обмен даёт HTTP 500.
|
||
func igateChannelState(ctx context.Context, base, code string) (found, enabled bool, err error) {
|
||
base = strings.TrimRight(pickStr(strings.TrimSpace(base), igateDefaultURL), "/")
|
||
cctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||
defer cancel()
|
||
req, _ := http.NewRequestWithContext(cctx, http.MethodGet, base+"/api/admin/channels/wsl", nil)
|
||
resp, err := http.DefaultClient.Do(req)
|
||
if err != nil {
|
||
return false, false, err
|
||
}
|
||
defer resp.Body.Close()
|
||
if resp.StatusCode != http.StatusOK {
|
||
return false, false, fmt.Errorf("HTTP %s", resp.Status)
|
||
}
|
||
var list []struct {
|
||
Code string `json:"code"`
|
||
IsEnabled bool `json:"isEnabled"`
|
||
}
|
||
if err := json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&list); err != nil {
|
||
return false, false, err
|
||
}
|
||
for _, c := range list {
|
||
if strings.EqualFold(c.Code, code) {
|
||
return true, c.IsEnabled, nil
|
||
}
|
||
}
|
||
return false, false, nil
|
||
}
|
||
|
||
func registerIgate(mux *http.ServeMux, rc *RuntimeConfig) {
|
||
registerIgateAdmin(mux, rc)
|
||
mux.HandleFunc("/admin/setup/igate/state", func(w http.ResponseWriter, r *http.Request) {
|
||
ok, msg := igateState(r.Context(), rc.Snapshot().NSD.IGWBaseURL)
|
||
jsonOut(w, map[string]any{"ok": ok, "msg": msg, "installed": igateInstalled()})
|
||
})
|
||
}
|
||
|
||
// ---- контуры НРД: TEST3 и боевой ----
|
||
|
||
func currentNSDProfile() string {
|
||
if globalRC == nil {
|
||
return ""
|
||
}
|
||
return globalRC.Snapshot().NSD.Profile
|
||
}
|
||
|
||
// contourOf — контур по профилю bj: prod | guest | test3.
|
||
func contourOf(profile string) string {
|
||
switch {
|
||
case strings.HasPrefix(profile, "prod"):
|
||
return "prod"
|
||
case strings.HasPrefix(profile, "guest"):
|
||
return "guest"
|
||
}
|
||
return "test3"
|
||
}
|
||
|
||
// contourChannel — начало кода канала ИШ контура (канал = <код><депозитарный код>).
|
||
func contourChannel(contour string) string {
|
||
switch contour {
|
||
case "prod":
|
||
return "PROD"
|
||
case "guest":
|
||
return "GUEST"
|
||
}
|
||
return "TEST3"
|
||
}
|
||
|
||
// igateExchangeDir — папка обмена канала ИШ контура (у боевого — своя, чтобы входящие контуров не смешивались).
|
||
func igateExchangeDir(profile string) string {
|
||
if contourOf(profile) == "prod" {
|
||
return "/var/lib/igate/exchange-prod"
|
||
}
|
||
return "/var/lib/igate/exchange"
|
||
}
|
||
|
||
const igateUploadDir = "/var/lib/bj/igate-upload"
|
||
|
||
var igateVersionRe = regexp.MustCompile(`^\d+\.\d+\.\d+$`)
|
||
|
||
func registerIgateAdmin(mux *http.ServeMux, rc *RuntimeConfig) {
|
||
// состояние ИШ и каналов + что выбрано в bj
|
||
mux.HandleFunc("/admin/setup/igate/status", func(w http.ResponseWriter, r *http.Request) {
|
||
// пока записывается канал, root-обработчик занят им (до двух минут) и запрос состояния ждал бы в очереди —
|
||
// страница после перезагрузки висела на «Проверяю…»; отдаём последнее известное состояние
|
||
if v := igateContourJob.view(); v.Running {
|
||
if last := igateLastStatus.get(); last != nil {
|
||
last["busy"] = "идёт запись канала " + strings.ToUpper(v.Contour)
|
||
jsonOut(w, last)
|
||
return
|
||
}
|
||
}
|
||
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
|
||
defer cancel()
|
||
res, err := runKeymediaOp(ctx, "igate-status", nil)
|
||
if err != nil {
|
||
jsonOut(w, map[string]any{"ok": false, "error": err.Error()})
|
||
return
|
||
}
|
||
defer igateLastStatus.set(res)
|
||
s := rc.Snapshot()
|
||
res["active_contour"] = ""
|
||
if s.NSD.Profile != "" {
|
||
res["active_contour"] = contourOf(s.NSD.Profile)
|
||
}
|
||
res["active_profile"] = s.NSD.Profile
|
||
res["deponent"] = pickStr(s.NSD.DeponentCode, orgCode(s))
|
||
res["depo_db"] = s.Fancy.DepoDB()
|
||
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
|
||
mux.HandleFunc("/admin/setup/igate/install", func(w http.ResponseWriter, r *http.Request) {
|
||
if r.Method != http.MethodPost {
|
||
http.Error(w, "method", http.StatusMethodNotAllowed)
|
||
return
|
||
}
|
||
params := map[string]string{}
|
||
if f, hdr, err := r.FormFile("file"); err == nil {
|
||
defer f.Close()
|
||
name := strings.ToLower(hdr.Filename)
|
||
if !strings.HasSuffix(name, ".zip") && !strings.HasSuffix(name, ".deb") {
|
||
jsonOut(w, map[string]any{"ok": false, "error": "нужен дистрибутив ИШ от НРД: igate-…-amd64.zip (или .deb из него)"})
|
||
return
|
||
}
|
||
_ = os.MkdirAll(igateUploadDir, 0o750)
|
||
dst := filepath.Join(igateUploadDir, "igate"+filepath.Ext(name))
|
||
out, err := os.Create(dst)
|
||
if err != nil {
|
||
jsonOut(w, map[string]any{"ok": false, "error": err.Error()})
|
||
return
|
||
}
|
||
_, err = io.Copy(out, io.LimitReader(f, 600<<20))
|
||
out.Close()
|
||
if err != nil {
|
||
jsonOut(w, map[string]any{"ok": false, "error": "загрузка: " + err.Error()})
|
||
return
|
||
}
|
||
params["file"] = dst
|
||
} else if r.FormValue("bundled") == "1" { // проверенный дистрибутив на сервере (образ или обновление bj)
|
||
file, _ := igateDistFile()
|
||
if file == "" {
|
||
jsonOut(w, map[string]any{"ok": false, "error": "дистрибутива ИШ на сервере нет — он приходит с обновлением bj"})
|
||
return
|
||
}
|
||
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"})
|
||
return
|
||
}
|
||
extra, _ := json.Marshal(params)
|
||
ctx, cancel := context.WithTimeout(r.Context(), time.Minute)
|
||
defer cancel()
|
||
jsonOut(w, opResult(runKeymediaOp(ctx, "igate-install", map[string]string{"extra": string(extra)})))
|
||
})
|
||
// откат ИШ к версии из последней резервной копии (её делает каждая установка/обновление)
|
||
mux.HandleFunc("/admin/setup/igate/rollback", func(w http.ResponseWriter, r *http.Request) {
|
||
if r.Method != http.MethodPost {
|
||
http.Error(w, "method", http.StatusMethodNotAllowed)
|
||
return
|
||
}
|
||
ctx, cancel := context.WithTimeout(r.Context(), time.Minute)
|
||
defer cancel()
|
||
jsonOut(w, opResult(runKeymediaOp(ctx, "igate-rollback", nil)))
|
||
})
|
||
// ход установки: этап, сообщение, журнал и сколько секунд идёт текущий этап (по времени записи файла)
|
||
mux.HandleFunc("/admin/setup/igate/install-status", func(w http.ResponseWriter, r *http.Request) {
|
||
jsonOut(w, igateInstallStatusJSON(rc))
|
||
})
|
||
// канал контура в ИШ (адрес, отпечаток УЦ, папка обмена); ключ — мастером установки ключа
|
||
mux.HandleFunc("/admin/setup/igate/contour", func(w http.ResponseWriter, r *http.Request) {
|
||
if r.Method != http.MethodPost {
|
||
http.Error(w, "method", http.StatusMethodNotAllowed)
|
||
return
|
||
}
|
||
s := rc.Snapshot()
|
||
contour, crypto := r.FormValue("contour"), r.FormValue("crypto")
|
||
if contour != "prod" {
|
||
contour = "test3"
|
||
}
|
||
if crypto != "rsa" {
|
||
crypto = "gost"
|
||
}
|
||
dep := pickStr(s.NSD.DeponentCode, orgCode(s))
|
||
if dep == "" {
|
||
jsonOut(w, map[string]any{"ok": false, "error": "сначала укажите депозитарный код организации (мастер, раздел «Организация»)"})
|
||
return
|
||
}
|
||
if !igateContourJob.start(contour) {
|
||
jsonOut(w, map[string]any{"ok": true, "running": true}) // уже идёт — страница покажет ход
|
||
return
|
||
}
|
||
go igateContourRun(rc, contour, crypto, dep)
|
||
jsonOut(w, map[string]any{"ok": true, "started": true})
|
||
})
|
||
registerIgateShare(mux, rc) // папки обмена шлюза по сети (igateshare.go)
|
||
// ход записи канала: страница показывает его и после перезагрузки
|
||
mux.HandleFunc("/admin/setup/igate/contour-status", func(w http.ResponseWriter, r *http.Request) {
|
||
jsonOut(w, igateContourJob.view())
|
||
})
|
||
// переключить bj на контур: профиль отправки, база DEPO контура; bj перезапускается
|
||
mux.HandleFunc("/admin/setup/igate/switch", func(w http.ResponseWriter, r *http.Request) {
|
||
if r.Method != http.MethodPost {
|
||
http.Error(w, "method", http.StatusMethodNotAllowed)
|
||
return
|
||
}
|
||
contour, crypto := r.FormValue("contour"), r.FormValue("crypto")
|
||
if contour != "prod" {
|
||
contour = "test3"
|
||
}
|
||
if crypto != "rsa" {
|
||
crypto = "gost"
|
||
}
|
||
s := rc.Snapshot()
|
||
dep := pickStr(s.NSD.DeponentCode, orgCode(s))
|
||
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
|
||
defer cancel()
|
||
// канал контура должен быть в ИШ и включён (ключ контура установлен)
|
||
st, err := runKeymediaOp(ctx, "igate-status", nil)
|
||
if err != nil {
|
||
jsonOut(w, map[string]any{"ok": false, "error": "состояние ИШ: " + err.Error()})
|
||
return
|
||
}
|
||
want := contourChannel(contour) + dep
|
||
ready := false
|
||
if chans, ok := st["channels"].([]any); ok {
|
||
for _, c := range chans {
|
||
if m, ok := c.(map[string]any); ok && m["code"] == want && m["enabled"] == true {
|
||
ready = true
|
||
}
|
||
}
|
||
}
|
||
if !ready {
|
||
jsonOut(w, map[string]any{"ok": false, "error": "в ИШ нет включённого канала " + want + ": настройте канал контура и установите ключ этого контура"})
|
||
return
|
||
}
|
||
if contour == "prod" && r.FormValue("confirm") != "БОЕВОЙ" {
|
||
jsonOut(w, map[string]any{"ok": false, "error": "для перехода на боевой контур введите слово БОЕВОЙ"})
|
||
return
|
||
}
|
||
depoDB := strings.TrimSpace(r.FormValue("depo_db"))
|
||
if depoDB != "" && !validDBName(depoDB) && !(s.Fancy.IsFirebird() && validFBPath(depoDB)) { // у Firebird — путь к базе
|
||
jsonOut(w, map[string]any{"ok": false, "error": "имя базы DEPO — латинские буквы, цифры и _"})
|
||
return
|
||
}
|
||
n, f := s.NSD, s.Fancy
|
||
from := contourOf(n.Profile)
|
||
// запоминаем базу DEPO контура, с которого уходим, и берём базу контура, на который переходим
|
||
if from == "prod" {
|
||
n.DepoDBProd = f.DepoDB()
|
||
} else {
|
||
n.DepoDBTest = f.DepoDB()
|
||
}
|
||
target := depoDB
|
||
if target == "" {
|
||
if contour == "prod" {
|
||
target = n.DepoDBProd
|
||
} else {
|
||
target = n.DepoDBTest
|
||
}
|
||
}
|
||
n.Profile = contour + "-" + crypto
|
||
n.IGWBaseURL = pickStr(n.IGWBaseURL, igateDefaultURL)
|
||
n.SwitchedAt = time.Now()
|
||
nc := s.NSDCerts
|
||
nc.LastStore, nc.LastRows, nc.LastProfile = nil, nil, "" // справочник другого контура — перечитается после перезапуска
|
||
_ = rc.UpdateNSDCerts(nc)
|
||
if target != "" && target != f.DepoDB() {
|
||
f.DepoDatabase = target
|
||
f.DepoTablesCheck, f.DepoTablesCheckOK = "", false // таблицы и обработку в другой базе — проверить заново
|
||
f.DepoJobCheck, f.DepoJobCheckOK = "", false
|
||
f.DepoProcCheck, f.DepoProcCheckOK = "", false
|
||
if err := rc.UpdateFancy(f); err != nil {
|
||
jsonOut(w, map[string]any{"ok": false, "error": err.Error()})
|
||
return
|
||
}
|
||
}
|
||
if err := rc.UpdateNSD(n); err != nil {
|
||
jsonOut(w, map[string]any{"ok": false, "error": err.Error()})
|
||
return
|
||
}
|
||
log.Printf("nsd: контур переключён %s → %s (профиль %s, база DEPO %s)", from, contour, n.Profile, rc.Snapshot().Fancy.DepoDB())
|
||
go func() {
|
||
time.Sleep(2 * time.Second)
|
||
if _, err := restartUnit(context.Background(), "bj-server"); err != nil {
|
||
log.Printf("nsd: перезапуск bj-server после смены контура: %v", err)
|
||
}
|
||
}()
|
||
name := map[string]string{"prod": "боевой", "test3": "тестовый TEST3"}[contour]
|
||
msg := "bj переключён на " + name + " контур (база DEPO " + rc.Snapshot().Fancy.DepoDB() + ") и перезапускается — обновите страницу через 15 секунд"
|
||
if !rc.Snapshot().Fancy.DepoTablesCheckOK {
|
||
msg += ". База DEPO сменилась — пройдите в мастере раздел «Учёт DEPO» (таблицы обмена и обработка в этой базе)"
|
||
}
|
||
jsonOut(w, map[string]any{"ok": true, "output": msg})
|
||
})
|
||
}
|
||
|
||
// refreshCertStore — перечитать справочник сертификатов ИШ сейчас (после установки ключа, настройки канала,
|
||
// смены контура), не дожидаясь плановой сверки раз в 6 часов: от него зависят шаг «Сертификаты» и сроки.
|
||
func refreshCertStore(rc *RuntimeConfig) {
|
||
go func() {
|
||
time.Sleep(20 * time.Second) // ИШ перезапускается после изменений
|
||
// root-воркер обрабатывает запросы по одному и сразу после установки ключа
|
||
// (перезапуск ИШ, повторная активация профиля) бывает занят — одной попытки
|
||
// может не хватить, она отваливается по таймауту. Повторяем, пока сверка не
|
||
// пройдёт: без свежего справочника шаг «Ключ и сертификаты» остаётся
|
||
// невыполненным и в мастере нет кнопки «Далее», хотя ключ уже подписывает.
|
||
for attempt := 1; attempt <= 5; attempt++ {
|
||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||
_, err := nsdCertsCheck(ctx, rc)
|
||
cancel()
|
||
if err == nil {
|
||
return
|
||
}
|
||
log.Printf("nsdcerts: перечитать справочник (попытка %d/5): %v", attempt, err)
|
||
time.Sleep(30 * time.Second)
|
||
}
|
||
}()
|
||
}
|
||
|
||
// 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"
|
||
|
||
// 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
|
||
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 {
|
||
_, 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")
|
||
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, 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 (обновление ядра перезапускает его до того, как успел бы сработать первый вызов).
|
||
// 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 igateUpgradeSkipped(dist) { // ИШ откатили с этой версии вручную — сам не ставим, пока не придёт следующая
|
||
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
|
||
}
|
||
}
|
||
}
|