Files
Bridge-and-Join-s/internal/lkgateway/licensewarn.go
T

206 lines
9.1 KiB
Go

package lkgateway
// Предупреждения лицензии.
//
// 1. Заранее об окончании: за 30, 14, 7 и 1 день до окончания договора (или ключа, если договора
// нет или сервер лицензий перестал продлевать ключ). Каждый порог — одно уведомление; если
// договор продлили (дата сменилась), отсчёт начинается заново.
// 2. При закрытых обновлениях bj — о смене версии внешней системы (МНП QUIK, ИШ, DEPO, …).
//
// Решение 16.09.2026: после окончания лицензии фоновые процессы (цикл QUIK МНП, личный кабинет,
// обмен с НРД и DEPO) не останавливаются. Последствие одно — bj перестаёт обновляться, и новые
// версии внешних систем со временем могут перестать с ним корректно работать; об этом и уведомляем.
import (
"encoding/json"
"fmt"
"log"
"math"
"os"
"path/filepath"
"strings"
"sync"
"time"
)
// licenseWatchFile — что уже отправлено и последние известные версии внешних систем.
var licenseWatchFile = "/var/lib/bj/.bj/license-watch.json"
type licenseWatch struct {
EndDate string `json:"end_date,omitempty"` // дата окончания, по которой шли предупреждения
WarnedDays int `json:"warned_days,omitempty"` // последний отправленный порог (дней)
Versions map[string]string `json:"versions,omitempty"` // ключ системы → последняя известная версия
}
var (
licenseWatchMu sync.Mutex
licenseWarnSteps = []int{30, 14, 7, 1}
)
// externalSystems — внешние системы, с которыми работает bj (свои компоненты bj обновляются вместе с ним).
var externalSystems = map[string]bool{
"quik-mnp": true, "ish": true, "depo": true, "quikexport": true,
"validata": true, "java": true, "postgres": true, "os": true,
}
func loadLicenseWatch() licenseWatch {
var w licenseWatch
if b, err := os.ReadFile(licenseWatchFile); err == nil {
_ = json.Unmarshal(b, &w)
}
if w.Versions == nil {
w.Versions = map[string]string{}
}
return w
}
func saveLicenseWatch(w licenseWatch) {
b, _ := json.MarshalIndent(w, "", " ")
tmp := licenseWatchFile + ".tmp"
if err := os.MkdirAll(filepath.Dir(licenseWatchFile), 0o700); err == nil {
if err = os.WriteFile(tmp, b, 0o600); err == nil {
err = os.Rename(tmp, licenseWatchFile)
}
if err != nil {
log.Printf("lk-gateway: лицензия: запись %s: %v", licenseWatchFile, err)
}
}
}
// ---- 1. заранее об окончании ----
// licenseEnd — когда лицензия перестанет действовать и что именно заканчивается.
func licenseEnd(st LicenseStatus, lastSync, now time.Time) (end time.Time, what string) {
if !st.Valid {
return time.Time{}, "" // уже не действует — об этом говорит статус и отказ в продлении
}
if !st.ContractUntil.IsZero() {
end, what = st.ContractUntil, "поддержка"
}
renewStuck := st.Online && now.Sub(lastSync) > 48*time.Hour
if (!st.Online || renewStuck) && !st.ExpiresAt.IsZero() && (end.IsZero() || st.ExpiresAt.Before(end)) {
end, what = st.ExpiresAt, "ключ"
if renewStuck {
what = "ключ (сервер лицензий не продлевает его)"
}
}
return end, what
}
// expiryStep — какой порог отправить (0 — ничего): наименьший порог, в который уже попали, если он
// ещё не отправлен для этой даты окончания.
func expiryStep(daysLeft int, w licenseWatch, endDate string) int {
warned := w.WarnedDays
if w.EndDate != endDate {
warned = 0
}
step := 0
for _, s := range licenseWarnSteps {
if daysLeft <= s {
step = s
}
}
if step == 0 || (warned != 0 && step >= warned) {
return 0
}
return step
}
func ruDays(n int) string {
switch {
case n%10 == 1 && n%100 != 11:
return fmt.Sprintf("%d день", n)
case n%10 >= 2 && n%10 <= 4 && (n%100 < 12 || n%100 > 14):
return fmt.Sprintf("%d дня", n)
}
return fmt.Sprintf("%d дней", n)
}
// licenseExpiryWarn — вызывается раз в час из licenseLoop.
func licenseExpiryWarn(rc *RuntimeConfig, now time.Time) {
st := licenseStatus(rc)
end, what := licenseEnd(st, rc.Snapshot().License.LastSync, now)
if end.IsZero() {
return
}
endDate := end.Local().Format("2006-01-02")
daysLeft := int(math.Ceil(end.Sub(now).Hours() / 24))
if daysLeft < 0 {
daysLeft = 0
}
licenseWatchMu.Lock()
defer licenseWatchMu.Unlock()
w := loadLicenseWatch()
step := expiryStep(daysLeft, w, endDate)
dateChanged := w.EndDate != endDate
if dateChanged {
w.EndDate, w.WarnedDays = endDate, 0 // договор продлили или сменился ключ — отсчёт заново
}
if step != 0 {
w.WarnedDays = step
}
if dateChanged || step != 0 {
saveLicenseWatch(w)
}
if step == 0 {
return
}
left := "осталось " + ruDays(daysLeft)
if daysLeft == 0 {
left = "сегодня последний день"
}
if what == "поддержка" {
// лицензия бессрочная: по окончании поддержки всё работает, закрываются только новые версии
raiseSystemAlert(rc, "Поддержка bj заканчивается через "+ruDays(daysLeft),
fmt.Sprintf("Поддержка (обновления) bj действует до %s — %s. После этого bj и все купленные модули продолжат работать, "+
"переустановка останется доступной, но только на версиях, вышедших до этой даты. Новые версии bj (под новые версии МНП, ИШ НРД и других систем) "+
"станут доступны после продления поддержки — обратитесь в ZETIT.", end.Local().Format("02.01.2006"), left))
return
}
raiseSystemAlert(rc, "Лицензия: "+strings.SplitN(what, " ", 2)[0]+" заканчивается через "+ruDays(daysLeft),
fmt.Sprintf("Ключ лицензии bj (%s) действует до %s — %s. bj продлевает его автоматически через сервер лицензий; если продления не будет, "+
"модули по лицензии и обновления отключатся. Проверьте связь с lic.zetit.ru или обратитесь в ZETIT.", what, end.Local().Format("02.01.2006"), left))
}
// ---- 2. смена версии внешней системы при закрытых обновлениях ----
// versionChanges — сверка с последними известными версиями (запоминает новые); список изменений
// возвращается только когда обновления bj недоступны.
func versionChanges(w *licenseWatch, items []SystemVersion, updatesOK bool) []string {
var changed []string
for _, it := range items {
if !externalSystems[it.Key] || it.Version == "" {
continue
}
if prev := w.Versions[it.Key]; prev != "" && prev != it.Version && !updatesOK {
changed = append(changed, fmt.Sprintf("%s: %s → %s", it.Title, prev, it.Version))
}
w.Versions[it.Key] = it.Version
}
return changed
}
// licenseVersionWatch — вызывается после каждого опроса версий систем.
func licenseVersionWatch(rc *RuntimeConfig, v SystemVersions) {
st := licenseStatus(rc)
updatesOK := !licensingEnabled(rc) || (st.Valid && st.AllowsUpdates)
licenseWatchMu.Lock()
w := loadLicenseWatch()
before, _ := json.Marshal(w.Versions)
changed := versionChanges(&w, v.Items, updatesOK)
if after, _ := json.Marshal(w.Versions); string(after) != string(before) {
saveLicenseWatch(w)
}
licenseWatchMu.Unlock()
if len(changed) == 0 {
return
}
reason := "лицензия: " + st.Message
if st.Valid {
reason = "обновления не входят в лицензию"
}
raiseSystemAlert(rc, "Версия внешней системы изменилась, а обновления bj недоступны",
fmt.Sprintf("Изменилось: %s. Обновления bj сейчас недоступны (%s). bj продолжает работать, но совместимость с новой версией не проверена и не гарантируется — "+
"при сбоях обмена причина может быть в этом. Совместимое обновление bj придёт после продления лицензии.", strings.Join(changed, "; "), reason))
}