feat(bj): модуль ЛК на PostgreSQL, обмен с DEPO, боевой ЛК с имитатором НРД, QUIK, НПР
- bj_exchange и «кран» Fancy убраны: данные модуля ЛК в PostgreSQL (схема lk) - трансферные таблицы FTV_M2M: создание при установке, события, справочник участников - сверка с DEPO: счёт, ФИО, дата рождения, ИИС по договору, квал-инвестор из опции DEPO - боевой ЛК ЕСИА Финанс при тестовом ИШ: журнал, отказ через DEPO, статусы в ЛК - онлайн-лицензирование в bj, план тестирования M2M для контрагента (интерактивный HTML) - модули QUIK МНП, экспорт QUIK, НПР/УДС; документы и тестовые материалы Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package lkgateway
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@@ -29,7 +30,6 @@ func urlQ(s string) string { return url.QueryEscape(s) }
|
||||
// VDCrySvc) делает помощник /usr/local/sbin/bj-keymedia-install через узкий
|
||||
// sudoers (bj-server работает под непривилегированным bj).
|
||||
|
||||
|
||||
// keyWizardStep — один шаг мастера.
|
||||
type keyWizardStep struct {
|
||||
Title string
|
||||
@@ -232,6 +232,9 @@ func (h *setupHandlers) keyWizardUpload(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
password := r.FormValue("password")
|
||||
|
||||
// Не копим staging: чистим прошлые распаковки/архивы перед новой загрузкой.
|
||||
sweepStaging()
|
||||
|
||||
isoDir := "/var/lib/bj/iso"
|
||||
if err := os.MkdirAll(isoDir, 0o755); err != nil {
|
||||
h.keyWizFlash(w, r, "Не удалось создать "+isoDir+": "+err.Error())
|
||||
@@ -332,8 +335,11 @@ func (h *setupHandlers) keyWizardInstall(w http.ResponseWriter, r *http.Request)
|
||||
tgt, _ := res["target"].(string)
|
||||
spr, _ := res["spr"].(string)
|
||||
h.keyWiz.set(1, "ok", fmt.Sprintf("Профиль «%s» записан на %s. Бэкап: %s", h.keyWiz.Profile, fallback(tgt, "носитель"), h.keyWiz.Backup))
|
||||
crl, _ := res["crl"].(string)
|
||||
h.keyWiz.set(2, "ok", fmt.Sprintf("Справочник «%s» сформирован. CRL: %s", fallback(spr, "—"), crlRu(crl)))
|
||||
rebuild, _ := res["rebuild"].(string)
|
||||
igate, _ := res["igate"].(string)
|
||||
igp, _ := res["igate_profile_dir"].(string)
|
||||
h.keyWiz.set(2, "ok", fmt.Sprintf("Справочник «%s» пересобран (%s); профиль ИШ %s %s",
|
||||
fallback(spr, "—"), rebuildRu(rebuild), igateRu(igate), igp))
|
||||
h.keyWiz.set(3, "active", "")
|
||||
h.keyWiz.mu.Unlock()
|
||||
|
||||
@@ -354,6 +360,9 @@ func (h *setupHandlers) keyWizardInstall(w http.ResponseWriter, r *http.Request)
|
||||
h.keyWiz.Done = true
|
||||
h.keyWiz.mu.Unlock()
|
||||
|
||||
// Ключ уже на флешке и в ИШ — распаковка staging больше не нужна.
|
||||
_ = os.RemoveAll(staging)
|
||||
|
||||
http.Redirect(w, r, "/admin/setup/keywizard?flash="+urlQ("Готово! Ключ на флешке, справочник сформирован, ИШ перезапущен и работает. Финальная проверка — тестовым документом роботу."), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
@@ -370,7 +379,7 @@ func (h *setupHandlers) restartAndVerifyISH(ctx context.Context) (bool, string)
|
||||
deadline := time.Now().Add(40 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
if st := h.ishEngineState(ctx); st != "" {
|
||||
return true, "engine "+st
|
||||
return true, "engine " + st
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -418,6 +427,36 @@ func crlRu(s string) string {
|
||||
}
|
||||
}
|
||||
|
||||
// rebuildRu — статус пересборки справочника сертификатов на флешке.
|
||||
func rebuildRu(s string) string {
|
||||
switch {
|
||||
case s == "ok":
|
||||
return "совместимый формат, сертификаты извлечены из архива"
|
||||
case s == "skip":
|
||||
return "пропущено"
|
||||
case strings.HasPrefix(s, "fail"):
|
||||
return "ошибка пересборки (" + strings.TrimPrefix(s, "fail: ") + ")"
|
||||
default:
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
// igateRu — статус формирования профиля для ИШ.
|
||||
func igateRu(s string) string {
|
||||
switch s {
|
||||
case "connected":
|
||||
return "сформирован, зарегистрирован и подключён к ИШ (cryptography.profile):"
|
||||
case "ok":
|
||||
return "сформирован и зарегистрирован (но не выбран в ИШ):"
|
||||
case "skip":
|
||||
return "не формировался"
|
||||
case "ini-fail":
|
||||
return "сформирован, но регистрация в zpki.ini не удалась:"
|
||||
default:
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
// reactivateCryptoProfile повторно активирует текущий серверный профиль
|
||||
// bj-crypto (после перезапуска VDCrySvc активация в сайдкаре сбрасывается).
|
||||
// Best-effort: возвращает true при успехе.
|
||||
@@ -448,6 +487,24 @@ func boolRu(b bool, yes, no string) string {
|
||||
return no
|
||||
}
|
||||
|
||||
// sweepStaging удаляет прошлые распаковки (/var/lib/bj/media/iso/*) и архивы
|
||||
// (/var/lib/bj/iso/*.7z|zip), чтобы staging не копился. Вызывается перед новой
|
||||
// загрузкой — текущая распаковка создаётся уже после очистки и не страдает.
|
||||
func sweepStaging() {
|
||||
if entries, err := os.ReadDir("/var/lib/bj/media/iso"); err == nil {
|
||||
for _, e := range entries {
|
||||
_ = os.RemoveAll(filepath.Join("/var/lib/bj/media/iso", e.Name()))
|
||||
}
|
||||
}
|
||||
for _, pat := range []string{"/var/lib/bj/iso/*.7z", "/var/lib/bj/iso/*.zip"} {
|
||||
if files, err := filepath.Glob(pat); err == nil {
|
||||
for _, f := range files {
|
||||
_ = os.Remove(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const keymediaReqDir = "/var/lib/bj/keymedia-requests"
|
||||
|
||||
// runKeymediaHelper передаёт запрос привилегированному воркеру через файловый
|
||||
@@ -456,15 +513,23 @@ const keymediaReqDir = "/var/lib/bj/keymedia-requests"
|
||||
// и пишет <id>.res. bj-server опрашивает результат. Так привилегированная
|
||||
// работа идёт вне mount-namespace песочницы, где доступно перемонтирование USB.
|
||||
func runKeymediaHelper(ctx context.Context, action, staging, profile, targetDev, targetMnt string) (map[string]any, error) {
|
||||
return runKeymediaOp(ctx, action, map[string]string{"staging": staging, "profile": profile, "target_dev": targetDev, "target_mnt": targetMnt})
|
||||
}
|
||||
|
||||
// runKeymediaOp — общий вход к root-воркеру: action + произвольные поля
|
||||
// запроса (staging, profile, target_dev, target_mnt, extra). Используется и
|
||||
// мастером ключа, и автообновлением сертификатов НРД (nsdcerts.go).
|
||||
func runKeymediaOp(ctx context.Context, action string, fields map[string]string) (map[string]any, error) {
|
||||
id := fmt.Sprintf("%s-%d", action, time.Now().UnixNano())
|
||||
reqPath := filepath.Join(keymediaReqDir, id+".req")
|
||||
resPath := filepath.Join(keymediaReqDir, id+".res")
|
||||
defer os.Remove(resPath)
|
||||
|
||||
reqBody, _ := json.Marshal(map[string]string{
|
||||
"action": action, "staging": staging, "profile": profile,
|
||||
"target_dev": targetDev, "target_mnt": targetMnt,
|
||||
})
|
||||
req := map[string]string{"action": action}
|
||||
for k, v := range fields {
|
||||
req[k] = v
|
||||
}
|
||||
reqBody, _ := json.Marshal(req)
|
||||
// Пишем атомарно (tmp → rename), чтобы .path не подхватил полупустой файл.
|
||||
tmp := reqPath + ".tmp"
|
||||
if err := os.WriteFile(tmp, reqBody, 0o660); err != nil {
|
||||
@@ -487,6 +552,10 @@ func runKeymediaHelper(ctx context.Context, action, staging, profile, targetDev,
|
||||
continue // ещё не готово
|
||||
}
|
||||
res := map[string]any{}
|
||||
// systemd/скрипты могут напечатать предупреждение перед JSON — берём с первой «{».
|
||||
if i := bytes.IndexByte(b, '{'); i > 0 {
|
||||
b = b[i:]
|
||||
}
|
||||
if jerr := json.Unmarshal(b, &res); jerr != nil {
|
||||
return nil, fmt.Errorf("разбор ответа воркера: %v (%s)", jerr, strings.TrimSpace(string(b)))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user