Первичная настройка, повторная проверка: копии без ключевых носителей СКЗИ (--one-file-system, без keymedia), без отпечатка железа и списка компонентов; восстановление на новый сервер — пароль базы bj из копии, база и пользователь ИШ; ключ для контура без канала запоминается и подключается при настройке канала; первый настроенный канал становится рабочим контуром; справочник сертификатов перечитывается после ключа и смены контура; шлюз НРД — шаг 7, ключ — шаг 8 (ключ ставится в профиль ИШ); подсказка контура ключа по сертификату; ИШ: параметры работающего шлюза не перезаписываются; без чужого кода отправителя и получателя по умолчанию; прокси для выхода в интернет; установка с образа без интернета; HTTPS — новый сертификат при смене адреса; bj-network — без cloud-init; проверка готовности: справочник участников, сетевые доступы, учётные записи, последний обмен с НРД
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
@@ -7,7 +7,9 @@ package lkgateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"sort"
|
||||
@@ -119,7 +121,7 @@ func readinessChecks(ctx context.Context, rc *RuntimeConfig) []ReadyCheck {
|
||||
return c
|
||||
})
|
||||
run(func() ReadyCheck {
|
||||
c := ReadyCheck{Order: 7, Title: "Сертификат ключа организации", Link: "/admin/wizard?step=7", Required: true}
|
||||
c := ReadyCheck{Order: 8, Title: "Сертификат ключа организации", Link: "/admin/wizard?step=8", Required: true}
|
||||
own := ownValidCerts(storeCertsForUI(s))
|
||||
if len(own) == 0 {
|
||||
c.State, c.Detail = "err", "в справочнике ИШ нет действующего сертификата организации — установите ключ мастером установки ключа"
|
||||
@@ -138,7 +140,7 @@ func readinessChecks(ctx context.Context, rc *RuntimeConfig) []ReadyCheck {
|
||||
return c
|
||||
})
|
||||
run(func() ReadyCheck {
|
||||
c := ReadyCheck{Order: 8, Title: "Шлюз НРД", Link: "/admin/wizard?step=8", Required: true}
|
||||
c := ReadyCheck{Order: 7, Title: "Шлюз НРД", Link: "/admin/wizard?step=7", Required: true}
|
||||
up, msg := igateState(ctx, s.NSD.IGWBaseURL)
|
||||
switch {
|
||||
case !up:
|
||||
@@ -147,6 +149,12 @@ func readinessChecks(ctx context.Context, rc *RuntimeConfig) []ReadyCheck {
|
||||
c.State, c.Detail = "err", "шлюз отвечает, но контур НРД в bj не выбран"
|
||||
default:
|
||||
c.State, c.Detail = "ok", msg+"; контур "+s.NSD.Profile
|
||||
nsdWatch.Lock()
|
||||
okAt := nsdWatch.LastPollOK
|
||||
nsdWatch.Unlock()
|
||||
if !okAt.IsZero() {
|
||||
c.Detail += "; последний успешный обмен с НРД " + okAt.Format("02.01 15:04")
|
||||
}
|
||||
if down, det := nsdLinkStatus(); down {
|
||||
c.State, c.Detail = "err", det
|
||||
}
|
||||
@@ -221,11 +229,105 @@ func readinessChecks(ctx context.Context, rc *RuntimeConfig) []ReadyCheck {
|
||||
}
|
||||
return c
|
||||
})
|
||||
run(func() ReadyCheck {
|
||||
c := ReadyCheck{Order: 13, Title: "Справочник участников M2M НРД", Link: "/admin/handbook"}
|
||||
raw, _ := readHandbook()
|
||||
at := handbookAt(raw)
|
||||
var hb struct {
|
||||
Participants []HandbookEntry `json:"participants"`
|
||||
}
|
||||
_ = json.Unmarshal(raw, &hb)
|
||||
switch {
|
||||
case len(hb.Participants) == 0:
|
||||
c.State, c.Detail = "err", "справочника нет — получателя перевода не определить"
|
||||
case !at.IsZero() && time.Since(at) > 14*24*time.Hour:
|
||||
c.State, c.Detail = "warn", fmt.Sprintf("%d участников, от %s — давно не обновлялся (обновляется по связи с НРД)", len(hb.Participants), at.Format("02.01.2006"))
|
||||
default:
|
||||
c.State, c.Detail = "ok", fmt.Sprintf("%d участников, от %s", len(hb.Participants), at.Format("02.01.2006"))
|
||||
}
|
||||
return c
|
||||
})
|
||||
run(func() ReadyCheck { return readyNetwork(ctx, s) })
|
||||
run(func() ReadyCheck {
|
||||
c := ReadyCheck{Order: 15, Title: "Учётные записи", Link: "/admin/setup#users"}
|
||||
var admins, others int
|
||||
for _, u := range globalUsers.List() {
|
||||
if !u.Active {
|
||||
continue
|
||||
}
|
||||
if u.Role == "admin" {
|
||||
admins++
|
||||
} else {
|
||||
others++
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case s.Auth.ADEnabled:
|
||||
c.State, c.Detail = "ok", fmt.Sprintf("вход через Active Directory; локальных: администраторов %d, других %d", admins, others)
|
||||
case others == 0:
|
||||
c.State, c.Detail = "warn", fmt.Sprintf("только администратор (%d) — заведите учётные записи операторов: у каждого свой вход, в журнале видно, кто что сделал", admins)
|
||||
default:
|
||||
c.State, c.Detail = "ok", fmt.Sprintf("администраторов %d, других пользователей %d", admins, others)
|
||||
}
|
||||
return c
|
||||
})
|
||||
wg.Wait()
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Order < out[j].Order })
|
||||
return out
|
||||
}
|
||||
|
||||
// readyNetwork — сетевые доступы, без которых обмен не заработает: контур НРД (ИШ ходит туда сам по ГОСТ-TLS —
|
||||
// здесь только TCP), справочник сертификатов Мосбиржи (LDAP) и сервер лицензий (ключ, продление, обновления).
|
||||
func readyNetwork(ctx context.Context, s Settings) ReadyCheck {
|
||||
c := ReadyCheck{Order: 14, Title: "Сетевые доступы", Link: "/admin/setup#server"}
|
||||
type target struct{ name, addr string }
|
||||
var ts []target
|
||||
switch contourOf(s.NSD.Profile) {
|
||||
case "prod":
|
||||
if strings.HasSuffix(s.NSD.Profile, "rsa") {
|
||||
ts = append(ts, target{"НРД (боевой, RSA)", "edor.nsd.ru:443"}, target{"справочник Мосбиржи (LDAP)", "vcert.pki.moex.com:50003"})
|
||||
} else {
|
||||
ts = append(ts, target{"НРД (боевой, ГОСТ)", "edog.nsd.ru:443"}, target{"справочник Мосбиржи (LDAP)", "vcert.pki.moex.com:50001"})
|
||||
}
|
||||
default:
|
||||
if strings.HasSuffix(s.NSD.Profile, "rsa") {
|
||||
ts = append(ts, target{"НРД (TEST3, RSA)", "rsa-t3.nsd.ru:443"}, target{"справочник Мосбиржи (LDAP)", "vcert.pki.moex.com:50007"})
|
||||
} else {
|
||||
ts = append(ts, target{"НРД (TEST3, ГОСТ)", "gost-t3.nsd.ru:443"}, target{"справочник Мосбиржи (LDAP)", "vcert.pki.moex.com:50005"})
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(s.Proxy.Address) == "" { // через прокси прямого TCP не будет — сервер лицензий проверяет его работа
|
||||
ts = append(ts, target{"сервер лицензий", "lic.zetit.ru:443"})
|
||||
}
|
||||
var bad, good []string
|
||||
var mu sync.Mutex
|
||||
var wg sync.WaitGroup
|
||||
for _, t := range ts {
|
||||
wg.Add(1)
|
||||
go func(t target) {
|
||||
defer wg.Done()
|
||||
d := net.Dialer{Timeout: 4 * time.Second}
|
||||
conn, err := d.DialContext(ctx, "tcp", t.addr)
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if err != nil {
|
||||
bad = append(bad, t.name+" ("+t.addr+")")
|
||||
return
|
||||
}
|
||||
conn.Close()
|
||||
good = append(good, t.name)
|
||||
}(t)
|
||||
}
|
||||
wg.Wait()
|
||||
sort.Strings(bad)
|
||||
if len(bad) > 0 {
|
||||
c.State, c.Detail = "err", "нет доступа: "+strings.Join(bad, ", ")+" — откройте эти адреса на межсетевом экране организации"
|
||||
return c
|
||||
}
|
||||
c.State, c.Detail = "ok", "доступны: "+strings.Join(good, ", ")
|
||||
return c
|
||||
}
|
||||
|
||||
func readyDepo(ctx context.Context, rc *RuntimeConfig) ReadyCheck {
|
||||
f := rc.Snapshot().Fancy
|
||||
c := ReadyCheck{Order: 3, Title: "Учёт DEPO", Link: "/admin/wizard?step=3&sub=done", Required: true}
|
||||
@@ -273,6 +375,19 @@ func readyDepo(ctx context.Context, rc *RuntimeConfig) ReadyCheck {
|
||||
return c
|
||||
}
|
||||
|
||||
// lastReadiness — итог последней проверки готовности: по нему отмечается шаг 10 мастера.
|
||||
var lastReadiness struct {
|
||||
sync.Mutex
|
||||
ready bool
|
||||
at time.Time
|
||||
}
|
||||
|
||||
func readinessDone() bool {
|
||||
lastReadiness.Lock()
|
||||
defer lastReadiness.Unlock()
|
||||
return lastReadiness.ready && time.Since(lastReadiness.at) < 24*time.Hour
|
||||
}
|
||||
|
||||
func registerReadiness(mux *http.ServeMux, rc *RuntimeConfig) {
|
||||
mux.HandleFunc("/admin/setup/readiness", func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
|
||||
@@ -284,6 +399,9 @@ func registerReadiness(mux *http.ServeMux, rc *RuntimeConfig) {
|
||||
ready = false
|
||||
}
|
||||
}
|
||||
lastReadiness.Lock()
|
||||
lastReadiness.ready, lastReadiness.at = ready, time.Now()
|
||||
lastReadiness.Unlock()
|
||||
jsonOut(w, map[string]any{"checks": checks, "ready": ready, "at": time.Now().Format("02.01.2006 15:04:05")})
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user