Files
Bridge-and-Join-s/internal/lkgateway/igwwatch.go
zuevav c62225dd7d ИШ ждёт флешку с ключом — bj это видит и перезапускает шлюз сам; итог установки ключа; список копий; скрытые блоки
- ИШ без флешки ждёт её в окне Валидаты («Вставьте ключевой носитель»): статус «ждёт флешку», без кнопки
  переустановки; сторож не тратит перезапуски, а когда флешка появилась — перезапускает ИШ сам; письмо
- шаг ключа: итог — карточка «Ключ подключён», «Заменить ключ» вместо «Установить ещё один», этапы свёрнуты
- копии: «Копии на хранилище» ищет там же, куда ложится копия (подпапка по умолчанию bj-backup, не из старых
  настроек); поля показываются разобранными
- [hidden] прячет всегда (пустая полоска под версией ИШ); «Шлюз НРД на другом сервере?» — что это за адрес

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
2026-10-07 00:11:48 +03:00

270 lines
11 KiB
Go
Raw Permalink 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
// igwwatch.go — сторож интеграционного шлюза НРД (ИШ). ИШ может «жить» как процесс
// (systemd считает службу active), но не слушать порт: тогда bj не получает входящие
// и не может отправить ни одного пакета. Сторож каждые 30 с проверяет, отвечает ли ИШ
// по HTTP, и:
// - после 2 мин недоступности — оповещение (лента «Новости» + e-mail/Zabbix) и
// попытка перезапустить службу igate (если ИШ на этом сервере; через root-демон
// bj-keymedia, список служб закрытый);
// - перезапуск не помог — оповещение с числом попыток; повтор раз в 20 мин, до 3
// попыток за эпизод, дальше напоминание раз в 2 ч;
// - ИШ снова отвечает — оповещение «восстановлен» с длительностью простоя.
// Состояние видно в шапке (индикатор «НРД на связи» → «ИШ НРД недоступен»).
import (
"context"
"fmt"
"log"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"sync"
"time"
)
const (
igwCheckEvery = 30 * time.Second
igwAlertAfter = 2 * time.Minute
igwRestartEvery = 20 * time.Minute
igwRestartMax = 3
igwRemindEvery = 2 * time.Hour
igwRestartSettle = 4 * time.Minute
igwServiceUnit = "igate"
)
type igwWatchState struct {
mu sync.Mutex
Checked bool
Up bool
DownSince time.Time
LastOK time.Time
LastErr string
Alerted bool
Restarts int
LastRestart time.Time
LastRemind time.Time
RestartMsg string
// KeyWait — ИШ ждёт флешку с ключом подписи (окно Валидаты «Вставьте ключевой носитель»): перезапуск не
// поможет, пока её нет, а когда она появится — bj перезапустит ИШ сам.
KeyWait bool
}
var igwWatch igwWatchState
// igwStatus — для шапки и страницы статуса: недоступен ли ИШ и сколько.
func igwStatus() (down bool, since time.Duration, detail string) {
igwWatch.mu.Lock()
defer igwWatch.mu.Unlock()
if !igwWatch.Checked || igwWatch.Up {
return false, 0, ""
}
d := fmt.Sprintf("нет ответа с %s: %s", igwWatch.DownSince.Format("02.01 15:04"), igwWatch.LastErr)
if igwWatch.Restarts > 0 {
d += fmt.Sprintf("; попыток перезапуска: %d (%s)", igwWatch.Restarts, igwWatch.RestartMsg)
}
return true, time.Since(igwWatch.DownSince).Round(time.Minute), d
}
// igwProbe — отвечает ли ИШ по HTTP (любой HTTP-ответ = жив; отказ соединения/таймаут = нет).
func igwProbe(ctx context.Context, base string) error {
ctx, cancel := context.WithTimeout(ctx, 8*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, strings.TrimRight(base, "/")+"/", nil)
if err != nil {
return err
}
resp, err := (&http.Client{Transport: &http.Transport{Proxy: nil}}).Do(req)
if err != nil {
return err
}
resp.Body.Close()
return nil
}
func igwLocal(base string) bool {
u, err := url.Parse(base)
if err != nil {
return false
}
h := u.Hostname()
return h == "localhost" || h == "127.0.0.1" || h == "::1"
}
func humanDur(d time.Duration) string {
d = d.Round(time.Minute)
if d < time.Hour {
return fmt.Sprintf("%d мин", int(d.Minutes()))
}
if d < 48*time.Hour {
return fmt.Sprintf("%d ч %d мин", int(d.Hours()), int(d.Minutes())%60)
}
return fmt.Sprintf("%d сут %d ч", int(d.Hours())/24, int(d.Hours())%24)
}
// igwWatchLoop — фоновый сторож (запускается через superviseLoop).
func (s *Server) igwWatchLoop(ctx context.Context) {
healthRegister("igw-watch", 2*igwCheckEvery)
t := time.NewTicker(igwCheckEvery)
defer t.Stop()
for {
igwWatchTick(ctx, s.rc)
healthBeat("igw-watch")
select {
case <-ctx.Done():
return
case <-t.C:
}
}
}
func igwWatchTick(ctx context.Context, rc *RuntimeConfig) {
base := strings.TrimSpace(rc.Snapshot().NSD.IGWBaseURL)
if base == "" {
igwWatch.mu.Lock()
igwWatch.Checked = false
igwWatch.mu.Unlock()
return
}
err := igwProbe(ctx, base)
now := time.Now()
w := &igwWatch
w.mu.Lock()
w.Checked = true
if err == nil {
wasDown, since, alerted, restarts := !w.Up && !w.DownSince.IsZero(), w.DownSince, w.Alerted, w.Restarts
w.Up, w.LastOK, w.LastErr, w.DownSince, w.Alerted, w.Restarts, w.RestartMsg = true, now, "", time.Time{}, false, 0, ""
w.mu.Unlock()
if igwLocal(base) {
nsdLinkTick(rc, now)
}
if wasDown && alerted {
how := "сам"
if restarts > 0 {
how = fmt.Sprintf("после перезапуска bj (попыток: %d)", restarts)
}
log.Printf("igw-watch: ИШ снова отвечает (%s), простой %s", how, humanDur(now.Sub(since)))
raiseSystemAlert(rc, "ИШ НРД снова доступен", fmt.Sprintf("Интеграционный шлюз НРД (%s) отвечает — восстановился %s. Простой %s (с %s). Обмен с НРД возобновлён: входящие переводы будут получены, ожидающие отправки уйдут.",
base, how, humanDur(now.Sub(since)), since.Format("02.01 15:04")))
}
return
}
if w.Up || w.DownSince.IsZero() {
w.Up, w.DownSince = false, now
log.Printf("igw-watch: ИШ НРД не отвечает (%s): %v", base, err)
}
w.LastErr = shortErr(err)
down := now.Sub(w.DownSince)
needAlert := !w.Alerted && down >= igwAlertAfter
canRestart := igwLocal(base) && down >= igwAlertAfter && w.Restarts < igwRestartMax &&
(w.LastRestart.IsZero() || now.Sub(w.LastRestart) >= igwRestartEvery)
keyAlert := false
if igwLocal(base) {
switch keymediaState() {
case "missing": // без флешки ИШ ждёт её в окне Валидаты — перезапуски бесполезны
canRestart = false
if !w.KeyWait {
w.KeyWait, keyAlert = true, true
needAlert = false
w.Alerted, w.LastRemind = true, now
}
case "ok":
if w.KeyWait { // флешку вернули — перезапустить ИШ сразу, попытки — заново
w.KeyWait, w.Restarts, w.LastRestart = false, 0, time.Time{}
canRestart = true
}
}
}
remind := w.Alerted && !canRestart && w.Restarts >= igwRestartMax && now.Sub(w.LastRemind) >= igwRemindEvery
if needAlert {
w.Alerted, w.LastRemind = true, now
}
if remind {
w.LastRemind = now
}
if canRestart {
w.Restarts++
w.LastRestart = now
}
attempt, since, lastErr := w.Restarts, w.DownSince, w.LastErr
w.mu.Unlock()
if keyAlert {
raiseSystemAlert(rc, "ИШ НРД ждёт флешку с ключом подписи", fmt.Sprintf("Интеграционный шлюз НРД не запускается: флешки с ключом подписи в сервере нет (%s пуста) — Валидата ждёт ключевой носитель. Вставьте флешку с ключом в этот сервер: bj перезапустит шлюз сам. Переустанавливать ИШ не нужно.", keymediaDir))
}
if needAlert {
plan := "ИШ на другом сервере — перезапустите службу ИШ там."
if igwLocal(base) {
plan = "bj пробует перезапустить службу " + igwServiceUnit + " автоматически и сообщит результат."
}
raiseSystemAlert(rc, "ИШ НРД недоступен", fmt.Sprintf("Интеграционный шлюз НРД (%s) не отвечает с %s (%s): %s. Пока ИШ недоступен, входящие переводы не принимаются, отправка в НРД невозможна. %s",
base, since.Format("02.01 15:04"), humanDur(down), lastErr, plan))
}
if remind {
raiseSystemAlert(rc, "ИШ НРД всё ещё недоступен", fmt.Sprintf("ИШ (%s) не отвечает %s (с %s); автоматические перезапуски (%d) не помогли: %s. Нужна проверка администратором: journalctl -u %s, igate-cli.",
base, humanDur(down), since.Format("02.01 15:04"), igwRestartMax, lastErr, igwServiceUnit))
}
if canRestart {
go igwTryRestart(rc, base, attempt)
}
}
// keymediaDir — куда подключается флешка с ключом для ИШ (bj-keymedia-install, строка fstab по UUID).
const keymediaDir = "/var/lib/igate/keymedia"
// keymediaState — флешка с ключом для ИШ: "" — не настроена (ключ не с флешки), "ok" — на месте, "missing" — нет.
func keymediaState() string {
b, err := os.ReadFile("/etc/fstab")
if err != nil || !strings.Contains(string(b), " "+keymediaDir+" ") {
return ""
}
if m, _ := filepath.Glob(keymediaDir + "/vdkeys/*.vdk"); len(m) > 0 {
return "ok"
}
return "missing"
}
// igwTryRestart — перезапуск службы ИШ и проверка, поднялся ли он.
func igwTryRestart(rc *RuntimeConfig, base string, attempt int) {
log.Printf("igw-watch: перезапуск службы %s (попытка %d из %d)", igwServiceUnit, attempt, igwRestartMax)
out, err := restartUnit(context.Background(), igwServiceUnit)
msg := out
if err != nil {
msg = "служба не перезапущена: " + err.Error()
} else {
deadline := time.Now().Add(igwRestartSettle)
for time.Now().Before(deadline) {
time.Sleep(15 * time.Second)
if igwProbe(context.Background(), base) == nil {
msg = "перезапуск выполнен, ИШ ответил"
break
}
msg = fmt.Sprintf("служба перезапущена, но ИШ не отвечает %s после перезапуска", humanDur(igwRestartSettle))
}
}
igwWatch.mu.Lock()
igwWatch.RestartMsg = msg
up := igwWatch.Up
igwWatch.mu.Unlock()
log.Printf("igw-watch: попытка %d: %s", attempt, msg)
if strings.HasPrefix(msg, "перезапуск выполнен") || up {
return // «восстановлен» сообщит igwWatchTick
}
next := "следующая попытка через 20 мин"
if attempt >= igwRestartMax {
next = "попытки исчерпаны — нужна проверка администратором (journalctl -u " + igwServiceUnit + ")"
}
raiseSystemAlert(rc, fmt.Sprintf("Перезапуск ИШ НРД не помог (попытка %d из %d)", attempt, igwRestartMax),
fmt.Sprintf("bj перезапустил службу %s, но шлюз %s не отвечает: %s. %s.", igwServiceUnit, base, msg, next))
}
func shortErr(err error) string {
s := err.Error()
if i := strings.LastIndex(s, ": "); i > 0 && len(s) > 120 {
s = s[i+2:]
}
return s
}