feat: живой цикл M2M с НРД + мастер установки ключа на флешку

Инфраструктура M2M (живой обмен с НРД через ИШ):
- обработка M2MTransferResponse: ERROR(M2Mxx) → заявка Отклонена, сохранение
  ответа; INFO → ждём Decision; идемпотентность поллера
- fallback-корреляция ответов с нулевым GUID (M2M14/M2M17) по FIFO
- сырой XML ответа НРД в карточке заявки (для пересылки в ТП)
- тестовый пакет роботу приведён к эталону m2m_robot_samples (CostInfo=Yes,
  4 бумаги, IsolationStatus, DocumentSeries=сценарий); override паспорта
- редирект из теста сразу в карточку заявки

Мастер установки ключа Валидаты на флешку (admin/setup/keywizard):
- пошаговый: загрузка .7z+пароль → выбор флешки → запись → справочник
  сертификатов (CRL) → перезапуск+проверка ИШ → готово
- привилегированный воркер (bj-keymedia) в host-namespace через файл-обмен,
  bj-server остаётся в песочнице
- сохранение структуры профиля архива (spr<N>), перечисление съёмных USB

Прочее:
- пакет-доказательство для ТП НРД + форма регистрации участника M2M
- эталонные образцы робота (DOC/m2m_robot_samples)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
zuevav
2026-06-19 00:03:21 +03:00
parent 6e503433d4
commit 9737c787f9
110 changed files with 10771 additions and 1690 deletions
+204
View File
@@ -0,0 +1,204 @@
package lkgateway
import (
"context"
"fmt"
"log"
"os"
"time"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/release"
)
// BuildVersion — версия bj-server. Переопределяется при сборке:
//
// go build -ldflags "-X .../lkgateway.BuildVersion=1.2.0"
var BuildVersion = "0.1.0"
// DefaultUpdatePublicKey — публичный ключ артефактории, зашитый в релиз.
// Пустой в исходниках; подставляется при официальной сборке. Если задан в
// настройках (UpdateSettings.PublicKey) — приоритет у настроек.
var DefaultUpdatePublicKey = ""
// installPaths — куда устанавливать артефакты по логическому имени.
// Файлы не из этого списка при авто-обновлении пропускаются (скрипты/SQL
// обновляются отдельно, не на лету).
var installPaths = map[string]string{
"bj-server": "/opt/bj/bj-server",
"crypto-service.jar": "/opt/bj/crypto-service.jar",
}
// Updater — авто-обновление bj-server из артефактории (работает поверх rc).
type Updater struct{ rc *RuntimeConfig }
// NewUpdater создаёт Updater на текущем runtime-конфиге.
func NewUpdater(rc *RuntimeConfig) *Updater { return &Updater{rc: rc} }
// UpdateStatus — сводка для UI/handler.
type UpdateStatus struct {
Configured bool
CurrentVersion string
Available string
HasUpdate bool
Channel string
Notes string
Message string
}
func (u *Updater) updateClient() (*release.Client, error) {
cfg := u.rc.Snapshot().Update
pub := cfg.PublicKey
if pub == "" {
pub = DefaultUpdatePublicKey
}
if cfg.BaseURL == "" || pub == "" {
return nil, fmt.Errorf("обновления не настроены (нужны URL артефактории и публичный ключ)")
}
channel := cfg.Channel
if channel == "" {
channel = "stable"
}
return release.NewClient(cfg.BaseURL, channel, pub)
}
// CheckForUpdate скачивает манифест, проверяет подпись, сравнивает версии и
// сохраняет результат в настройки. Возвращает сводку.
func (u *Updater) CheckForUpdate(ctx context.Context) (UpdateStatus, error) {
st := UpdateStatus{CurrentVersion: BuildVersion, Channel: u.rc.Snapshot().Update.Channel}
cl, err := u.updateClient()
if err != nil {
st.Message = err.Error()
return st, nil // не настроено — не ошибка
}
st.Configured = true
m, err := cl.FetchManifest(ctx)
if err != nil {
st.Message = "проверка не удалась: " + err.Error()
u.saveCheckResult(st)
return st, err
}
st.Available = m.Version
st.Notes = m.Notes
st.HasUpdate = release.IsNewer(m.Version, BuildVersion)
if st.HasUpdate {
st.Message = fmt.Sprintf("доступна версия %s (текущая %s)", m.Version, BuildVersion)
} else {
st.Message = "установлена актуальная версия " + BuildVersion
}
u.saveCheckResult(st)
return st, nil
}
func (u *Updater) saveCheckResult(st UpdateStatus) {
cfg := u.rc.Snapshot().Update
cfg.LastCheck = time.Now().UTC()
cfg.LastResult = st.Message
cfg.Available = st.Available
cfg.Notes = st.Notes
if err := u.rc.SaveUpdateSettings(cfg); err != nil {
log.Printf("lk-gateway: сохранение результата проверки обновления: %v", err)
}
}
// ApplyUpdate скачивает обновлённые артефакты (с проверкой подписи манифеста
// и sha256 каждого файла), атомарно заменяет бинари и завершает процесс с
// ненулевым кодом — systemd (Restart=on-failure) поднимает новую версию.
func (u *Updater) ApplyUpdate(ctx context.Context) error {
// Гейт лицензией: если лицензирование включено — требуется валидная
// лицензия с фичей updates. Без лицензирования (открытый режим) — пропускаем.
if licensingEnabled(u.rc) {
ls := licenseStatus(u.rc)
if !ls.Valid {
return fmt.Errorf("обновления заблокированы — лицензия: %s", ls.Message)
}
if !ls.AllowsUpdates {
return fmt.Errorf("обновления не входят в план %q", ls.Plan)
}
}
cl, err := u.updateClient()
if err != nil {
return err
}
m, err := cl.FetchManifest(ctx)
if err != nil {
return fmt.Errorf("манифест: %w", err)
}
if !release.IsNewer(m.Version, BuildVersion) {
return fmt.Errorf("обновление не требуется (текущая %s, доступна %s)", BuildVersion, m.Version)
}
updated := 0
for _, a := range m.Artifacts {
dst, ok := installPaths[a.Name]
if !ok {
continue // скрипты/SQL не обновляем на лету
}
dir := dirOf(dst)
path, err := cl.DownloadArtifact(ctx, a, dir)
if err != nil {
return fmt.Errorf("скачивание %s: %w", a.Name, err)
}
// DownloadArtifact кладёт файл под именем a.File; если целевое имя
// иное — переименуем атомарно.
if path != dst {
if err := os.Rename(path, dst); err != nil {
return fmt.Errorf("установка %s: %w", a.Name, err)
}
}
log.Printf("lk-gateway: обновлён %s → %s (%s)", a.Name, dst, m.Version)
updated++
}
if updated == 0 {
return fmt.Errorf("в манифесте %s нет обновляемых бинарей", m.Version)
}
log.Printf("lk-gateway: обновление до %s применено (%d файлов), перезапуск через systemd…", m.Version, updated)
// Завершаемся с ненулевым кодом — systemd Restart=on-failure поднимет
// новый бинарь. Даём пару секунд на флаш логов/ответа.
go func() {
time.Sleep(800 * time.Millisecond)
os.Exit(42)
}()
return nil
}
// updateLoop — фоновая авто-проверка обновлений (если включена).
func (u *Updater) updateLoop(ctx context.Context) {
ticker := time.NewTicker(6 * time.Hour)
defer ticker.Stop()
check := func() {
if !u.rc.Snapshot().Update.AutoCheck {
return
}
cctx, cancel := context.WithTimeout(ctx, 90*time.Second)
defer cancel()
if st, err := u.CheckForUpdate(cctx); err == nil && st.HasUpdate {
log.Printf("lk-gateway: доступно обновление %s (текущая %s)", st.Available, st.CurrentVersion)
}
}
// первая проверка через минуту после старта
select {
case <-ctx.Done():
return
case <-time.After(time.Minute):
check()
}
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
check()
}
}
}
func dirOf(path string) string {
for i := len(path) - 1; i >= 0; i-- {
if path[i] == '/' {
return path[:i]
}
}
return "."
}