Files
Bridge-and-Join-s/internal/lkgateway/prepare.go
T
zuevav b4cfa65699 ИШ ставится при подготовке; на шаге «Шлюз НРД» — версия и «Установить заново»; ссылка на НРД вместо писем
- «Подготовка модулей»: ИШ нет на сервере — bj ставит его из дистрибутива на
  сервере (образ или обновление bj — версия, проверенная ZETIT), ход — в списке.
- Шаг «Шлюз НРД»: установлен и работает — версия; не стоит или не работает —
  одна кнопка «Установить ИШ» / «Установить ИШ заново» (систему переставлять не
  нужно). «Скачать с сайта НРД» и «Установить из файла» — только в режиме
  разработки: версию ИШ меняем после своей проверки, вместе с обновлением bj.
- Каналы TEST3 и боевого: до установки ИШ — «канал создаётся после установки
  ИШ», после — «Создать канал».
- «Письма в НРД о доступе к контурам» убраны: НРД подключает заявкой на сайте —
  ссылка на «MOEX МОСТ для M2M», раздел «Как подключиться».

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
2026-10-06 17:30:58 +03:00

379 lines
16 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package lkgateway
// «Подготовка модулей» (05.10.2026) — экран между вводом ключа продукта и мастером настройки. Образ ставит
// базовый набор (ядро); после ввода ключа bj узнаёт состав лицензии, ставит модули, которые в неё входят
// (файлы — из комплекта образа, если там тот же выпуск, иначе с сервера обновлений), запускает их службы
// и только потом открывает мастер: мастер настраивает уже установленное.
import (
"context"
"fmt"
"net/http"
"os"
"os/exec"
"strconv"
"strings"
"sync"
"time"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/release"
)
// PrepareItem — строка экрана: что входит в лицензию и в каком оно состоянии.
type PrepareItem struct {
Title string `json:"title"`
State string `json:"state"` // ok | run | wait | warn | err | info
Detail string `json:"detail,omitempty"`
}
var prepareRun struct {
sync.Mutex
Running, Finished bool
Result, Err string
UpdState, UpdText string // этап «Обновление bj»: пусто — ещё не проверялось в этом запуске bj
IgateErr string // установку ИШ не удалось даже запустить
}
// prepareBoot — метка запуска bj: страница видит перезапуск (после обновления ядра) и продолжает подготовку.
var prepareBoot = time.Now().UnixNano()
// prepareCoreFeatures — признаки лицензии, которые работают внутри ядра (отдельных файлов нет).
var prepareCoreFeatures = []string{"quik-mnp", "esia-finance"}
// prepareItems — состав лицензии этой установки и состояние каждого модуля.
func prepareItems(rc *RuntimeConfig, running, finished bool, runErr string) []PrepareItem {
prepareRun.Lock()
us, ut := prepareRun.UpdState, prepareRun.UpdText
prepareRun.Unlock()
if us == "" {
us, ut = "wait", "проверю, есть ли обновление"
}
out := []PrepareItem{{Title: "Обновление bj", State: us, Detail: ut},
{Title: "Ядро bj: обмен с НРД, переводы M2M", State: "ok", Detail: "установлено, версия " + BuildVersion}}
for _, f := range prepareCoreFeatures {
if moduleAllowed(rc, f) {
out = append(out, PrepareItem{Title: moduleTitles[f], State: "ok", Detail: "входит в ядро — устанавливать нечего"})
}
}
for _, c := range release.Components {
if !c.FromCore || !moduleAllowed(rc, c.Feature) {
continue
}
it := PrepareItem{Title: prepareTitle(c)}
_, statErr := os.Stat(c.Path)
present := statErr == nil
var u unitRun
if present && c.Unit != "" {
u = unitRunState(c.Unit)
}
switch {
case u.State == "active" && u.Up >= 5*time.Second:
it.State, it.Detail = "ok", "установлен, служба работает"
case (u.State == "activating" || u.State == "active") && u.Restarts >= 3:
it.State, it.Detail = "err", "установлен, но служба падает при запуске: "+unitFailCause(c)
case u.State == "activating" || u.State == "reloading" || u.State == "active":
it.State, it.Detail = "run", "установлен, служба запускается…"
case present && c.Unit == "bj-crypto":
it.State, it.Detail = "warn", "установлен, служба не запущена ("+u.State+") — обмену не мешает: пакеты для НРД подписывает шлюз"
case present:
it.State, it.Detail = "ok", "установлен; служба заработает, когда модуль будет настроен (глава «Запуск»)"
case running:
it.State, it.Detail = "run", "устанавливается…"
case runErr != "":
it.State, it.Detail = "err", "не установлен — "+runErr
case finished:
it.State, it.Detail = "warn", "не установлен: в текущем выпуске на сервере обновлений его нет — поставится обновлением"
default:
it.State, it.Detail = "wait", "будет установлен"
}
out = append(out, it)
}
out = append(out, prepareIgateItem())
if moduleAllowed(rc, "web-cabinet") {
out = append(out, PrepareItem{Title: moduleTitles["web-cabinet"], State: "info", Detail: "ставится на веб-сервер организации: дистрибутив и ключ кабинета — в мастере, глава «Запуск»"})
}
return out
}
// unitRun — служба: состояние, сколько работает после последнего запуска, сколько раз systemd её перезапускал.
type unitRun struct {
State string
Up time.Duration
Restarts int
}
func unitRunState(unit string) unitRun {
r := unitRun{State: "unknown"}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
out, err := exec.CommandContext(ctx, "systemctl", "show", unit, "--property=ActiveState,NRestarts,ActiveEnterTimestampMonotonic").Output()
if err != nil {
return r
}
var since int64
for _, line := range strings.Split(string(out), "\n") {
k, v, _ := strings.Cut(strings.TrimSpace(line), "=")
switch k {
case "ActiveState":
r.State = v
case "NRestarts":
r.Restarts, _ = strconv.Atoi(v)
case "ActiveEnterTimestampMonotonic":
since, _ = strconv.ParseInt(v, 10, 64)
}
}
if b, err := os.ReadFile("/proc/uptime"); err == nil && since > 0 {
if f := strings.Fields(string(b)); len(f) > 0 {
if up, err := strconv.ParseFloat(f[0], 64); err == nil {
r.Up = time.Duration(up*float64(time.Second)) - time.Duration(since)*time.Microsecond
}
}
}
return r
}
// unitFailCause — причина падения службы модуля по последней строке её журнала ошибок — понятными словами.
func unitFailCause(c release.Component) string {
logs := map[string]string{"bj-crypto": "/var/log/bj/crypto-service.err", "bj-npr": "/var/log/bj/bj-npr.err", "bj-quikexport": "/var/log/bj/bj-quikexport.err"}
b, err := os.ReadFile(logs[c.Unit])
if err != nil {
return "подробности — в журнале службы " + c.Unit
}
lines := strings.Split(strings.TrimSpace(string(b)), "\n")
last := strings.TrimSpace(lines[len(lines)-1])
if strings.Contains(string(b[max(0, len(b)-4000):]), "UnsupportedClassVersionError") {
return "на сервере слишком старая Java (нужна 17 или новее): sudo apt-get install openjdk-17-jre-headless — служба запустится сама"
}
if len(last) > 240 {
last = last[:240] + "…"
}
return last
}
// prepareTitle — название модуля для экрана.
func prepareTitle(c release.Component) string {
switch c.Name {
case "crypto-service.jar":
return "Проверка ключа подписи (Валидата)"
}
if t := moduleTitles[c.Feature]; t != "" {
return t
}
return c.Title
}
// prepareStart — поставить модули лицензии (один запуск за раз; повторный — пока идёт — ничего не делает).
func prepareStart(rc *RuntimeConfig) {
prepareRun.Lock()
if prepareRun.Running {
prepareRun.Unlock()
return
}
prepareRun.Running, prepareRun.Finished, prepareRun.Result, prepareRun.Err = true, false, "", ""
prepareRun.Unlock()
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 25*time.Minute)
defer cancel()
prepareUpdates(ctx, rc) // сначала — обновления (при обновлении ядра bj перезапустится, страница продолжит)
var msg, errText string
if _, err := os.Stat(keymediaReqDir); err != nil {
errText = "на сервере нет службы установки (bj-keymedia)"
} else if m, err := NewUpdater(rc).ensureSplitModules(ctx); err != nil {
errText = strings.TrimPrefix(err.Error(), "Установка модулей ")
} 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) {
prepareRun.Lock()
prepareRun.UpdState, prepareRun.UpdText = state, text
prepareRun.Unlock()
}
set("run", "проверяю, есть ли обновление…")
if _, err := os.Stat(keymediaReqDir); err != nil {
set("warn", "на сервере нет службы установки (bj-keymedia) — обновления не ставятся")
return
}
u := NewUpdater(rc)
cctx, cancel := context.WithTimeout(ctx, 90*time.Second)
st, err := u.CheckForUpdate(cctx)
cancel()
switch {
case err != nil:
set("warn", "проверить обновления не удалось ("+err.Error()+") — продолжаю с установленной версией")
case !st.Configured:
set("warn", st.Message)
case !st.HasUpdate:
set("ok", "обновлений нет, установлена последняя версия "+BuildVersion)
case prepareTried(st.Available):
// уже ставили этот выпуск при подготовке (bj перезапускался) — не по кругу
set("warn", "выпуск "+st.Available+" поставлен не полностью: "+st.Message+" — доустановить можно в «Настройка → Обновления»")
default:
set("run", "есть обновление "+st.Available+" — ставлю; если обновится ядро, bj перезапустится, страница подождёт")
if msg, err := u.ApplyUpdate(ctx, nil); err != nil {
set("warn", "обновление не установилось: "+err.Error()+" — продолжаю с установленной версией")
} else {
set("ok", "обновление "+st.Available+" установлено: "+msg)
}
}
}
// prepareTriedFile — какой выпуск подготовка уже ставила: после перезапуска bj тот же выпуск не ставится снова.
var prepareTriedFile = "/var/lib/bj/.bj/prepare-update-tried"
func prepareTried(ver string) bool {
b, _ := os.ReadFile(prepareTriedFile)
if strings.TrimSpace(string(b)) == ver {
return true
}
_ = os.WriteFile(prepareTriedFile, []byte(ver+"\n"), 0o640)
return false
}
// safeNext — куда идти после подготовки: только свои страницы (как return_to).
func safeNext(v string) string {
if strings.HasPrefix(v, "/admin/") && !strings.HasPrefix(v, "//") && !strings.ContainsAny(v, "\r\n") {
return v
}
return "/admin/wizard?step=12"
}
// PrepareData — данные страницы «Подготовка модулей».
type PrepareData struct {
page
Tenant, Plan, Until string
Next string
Flash string
}
func registerPrepare(mux *http.ServeMux, h *setupHandlers) {
mux.HandleFunc("/admin/setup/prepare", func(w http.ResponseWriter, r *http.Request) {
st := licenseStatus(h.rc)
if !st.Valid {
http.Redirect(w, r, "/admin/wizard?step=1", http.StatusSeeOther)
return
}
prepareRun.Lock()
if !prepareRun.Running { // новый заход (ключ сменили, модуль добавили) — прошлый итог не в счёт
prepareRun.Finished, prepareRun.Result, prepareRun.Err = false, "", ""
}
prepareRun.Unlock()
d := PrepareData{page: nowPage("Подготовка модулей", "wizard"), Tenant: st.Tenant, Plan: st.Plan, Until: st.Message,
Next: safeNext(r.URL.Query().Get("next")), Flash: r.URL.Query().Get("flash")}
render(w, h.tpl.a.prepare, d)
})
mux.HandleFunc("/admin/setup/prepare/start", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
if !licenseStatus(h.rc).Valid {
jsonOut(w, map[string]any{"ok": false, "error": "лицензия не активирована"})
return
}
prepareStart(h.rc)
jsonOut(w, map[string]any{"ok": true})
})
mux.HandleFunc("/admin/setup/prepare/status", func(w http.ResponseWriter, r *http.Request) {
prepareRun.Lock()
running, finished, res, errText := prepareRun.Running, prepareRun.Finished, prepareRun.Result, prepareRun.Err
prepareRun.Unlock()
items := prepareItems(h.rc, running, finished, errText)
pending := false // модули лицензии, которых на сервере ещё нет
for _, c := range release.Components {
if c.FromCore && moduleAllowed(h.rc, c.Feature) {
if _, err := os.Stat(c.Path); err != nil {
pending = true
}
}
}
updProg.Lock()
pg := updProg.s
updProg.Unlock()
busy := false // служба модуля ещё запускается
for _, it := range items {
if it.State == "run" {
busy = true
}
}
prepareRun.Lock()
if prepareRun.UpdState == "" {
pending = true // обновления в этом запуске bj ещё не проверялись
}
prepareRun.Unlock()
out := map[string]any{"running": running, "finished": finished, "items": items, "pending": pending, "busy": busy, "result": res, "error": errText, "boot": prepareBoot}
if running && pg.Active {
out["stage"], out["done"], out["total"] = pg.Stage, pg.Done, pg.Total
}
jsonOut(w, out)
})
}