17d8ec3988
- release: пакет обновления (ExtractBundle/ReadStagedManifest/WriteBundle), Artifact.MinCore, план блокирует модуль, которому нужно ядро новее, чем будет после установки - bj-release: модули получают min_core = версия релиза - настройки → Обновления: «Обновить из файла» (zip до 2 ГБ), фон как у сетевого обновления, причина блокировки в таблице компонентов Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q4YVo4UMopqT5DzicqpPjL
206 lines
8.9 KiB
Go
206 lines
8.9 KiB
Go
package release
|
|
|
|
// Компоненты bj, которые обновляются независимо. У каждого свой файл в манифесте релиза и своя
|
|
// версия (Artifact.Version), путь установки и служба systemd. Список общий для bj-server (что
|
|
// показывать и предлагать к обновлению), root-установщика cmd/bj-component-install (что разрешено
|
|
// ставить и куда) и cmd/bj-release (какие файлы исполняемые).
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Component — независимо обновляемая часть bj.
|
|
type Component struct {
|
|
Name string // логическое имя артефакта в манифесте
|
|
Title string
|
|
Path string // куда устанавливать
|
|
Unit string // служба systemd, перезапускаемая после установки ("" — без перезапуска)
|
|
Exec bool // исполняемый файл
|
|
Core bool // ядро: ставится всегда; модули — только если уже установлены на этом сервере
|
|
// FromCore — модуль выделен из bj-server: обновление ставит его и там, где его ещё нет, если он
|
|
// входит в лицензию (иначе после обновления ядра раздел модуля перестал бы работать).
|
|
FromCore bool
|
|
// Feature — признак лицензии, без которого сервер обновлений не отдаёт файл компонента.
|
|
Feature string
|
|
}
|
|
|
|
// Components — все компоненты bj. Порядок — порядок показа и установки (ядро перезапускается последним).
|
|
var Components = []Component{
|
|
{Name: "bj-server", Title: "Ядро bj: переводы M2M, DEPO, QUIK МНП, ЛК ЕСИА", Path: "/opt/bj/bj-server", Unit: "bj-server", Exec: true, Core: true, Feature: "updates"},
|
|
{Name: "bj-component-install", Title: "Установщик обновлений", Path: "/usr/local/sbin/bj-component-install", Exec: true, Core: true, Feature: "updates"},
|
|
{Name: "crypto-service.jar", Title: "Криптосервис", Path: "/opt/bj/crypto-service.jar", Unit: "bj-crypto", Feature: "updates"},
|
|
{Name: "bj-lk", Title: "Личный кабинет клиента", Path: "/opt/bj/bj-lk", Unit: "bj-lk", Exec: true, Feature: "web-cabinet"},
|
|
{Name: "bj-npr", Title: "Риск-монитор НПР / УДС", Path: "/opt/bj/bj-npr", Unit: "bj-npr", Exec: true, FromCore: true, Feature: "npr"},
|
|
{Name: "bj-quikexport", Title: "Экспорт QUIK", Path: "/opt/bj/bj-quikexport", Unit: "bj-quikexport", Exec: true, FromCore: true, Feature: "quik-export"},
|
|
}
|
|
|
|
// ComponentByName — компонент по имени артефакта.
|
|
func ComponentByName(name string) (Component, bool) {
|
|
for _, c := range Components {
|
|
if c.Name == name {
|
|
return c, true
|
|
}
|
|
}
|
|
return Component{}, false
|
|
}
|
|
|
|
// PathAt — путь установки относительно корня root ("" — настоящий корень; иначе проверка в песочнице).
|
|
func (c Component) PathAt(root string) string {
|
|
if root == "" {
|
|
return c.Path
|
|
}
|
|
return filepath.Join(root, c.Path)
|
|
}
|
|
|
|
// InstalledFile — установленные версии компонентов; пишет bj-component-install.
|
|
const InstalledFile = "/var/lib/bj/.bj/components.json"
|
|
|
|
// Installed — запись об установленном компоненте.
|
|
type Installed struct {
|
|
Version string `json:"version"`
|
|
SHA256 string `json:"sha256"`
|
|
InstalledAt time.Time `json:"installed_at"`
|
|
}
|
|
|
|
// LoadInstalled — установленные версии (пустая карта, если файла нет).
|
|
func LoadInstalled(path string) map[string]Installed {
|
|
m := map[string]Installed{}
|
|
if b, err := os.ReadFile(path); err == nil {
|
|
_ = json.Unmarshal(b, &m)
|
|
}
|
|
return m
|
|
}
|
|
|
|
// SaveInstalled — атомарная запись установленных версий.
|
|
func SaveInstalled(path string, m map[string]Installed) error {
|
|
b, err := json.MarshalIndent(m, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil {
|
|
return err
|
|
}
|
|
tmp := path + ".tmp"
|
|
if err := os.WriteFile(tmp, b, 0o644); err != nil {
|
|
return err
|
|
}
|
|
return os.Rename(tmp, path)
|
|
}
|
|
|
|
// ComponentState — компонент на этом сервере против манифеста.
|
|
type ComponentState struct {
|
|
Component
|
|
Present bool // файл установлен на этом сервере
|
|
Installed string // установленная версия ("" — неизвестна)
|
|
Available string // версия в манифесте ("" — компонента нет в релизе)
|
|
SameFile bool // установленный файл совпадает с файлом релиза
|
|
Update bool // можно обновить
|
|
New bool // модуль выделен из ядра и ставится впервые (FromCore)
|
|
// Blocked — почему обновлять нельзя: модулю нужно более новое ядро, чем будет после установки.
|
|
Blocked string
|
|
}
|
|
|
|
// Plan — что можно обновить. m == nil — только состояние установленного (без сети).
|
|
// selfVersion — версия работающего bj-server, если записи об установке ещё нет.
|
|
func Plan(root string, m *Manifest, installed map[string]Installed, selfVersion string) []ComponentState {
|
|
var out []ComponentState
|
|
// Версия ядра после установки: если ядро тоже обновляется этим релизом — его новая версия,
|
|
// иначе — установленная. С ней сверяются требования модулей (Artifact.MinCore).
|
|
coreAfter := selfVersion
|
|
if inst, ok := installed["bj-server"]; ok && inst.Version != "" {
|
|
coreAfter = inst.Version
|
|
}
|
|
if m != nil {
|
|
for _, a := range m.Artifacts {
|
|
if a.Name == "bj-server" {
|
|
v := a.Version
|
|
if v == "" {
|
|
v = m.Version
|
|
}
|
|
if coreAfter == "" || IsNewer(v, coreAfter) {
|
|
coreAfter = v
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for _, c := range Components {
|
|
st := ComponentState{Component: c}
|
|
sha := ""
|
|
if fi, err := os.Stat(c.PathAt(root)); err == nil && fi.Mode().IsRegular() {
|
|
st.Present = true
|
|
sha = cachedHash(c.PathAt(root), fi)
|
|
}
|
|
if inst, ok := installed[c.Name]; ok && inst.SHA256 != "" && inst.SHA256 == sha {
|
|
st.Installed = inst.Version
|
|
}
|
|
if st.Installed == "" && c.Core && st.Present {
|
|
st.Installed = selfVersion
|
|
}
|
|
if m != nil {
|
|
for _, a := range m.Artifacts {
|
|
if a.Name != c.Name {
|
|
continue
|
|
}
|
|
st.Available = a.Version
|
|
if st.Available == "" {
|
|
st.Available = m.Version
|
|
}
|
|
st.SameFile = sha != "" && a.SHA256 == sha
|
|
st.Update = (st.Present || c.Core) && !st.SameFile && (st.Installed == "" || IsNewer(st.Available, st.Installed))
|
|
if !c.Core && a.MinCore != "" && coreAfter != "" && IsNewer(a.MinCore, coreAfter) {
|
|
// Модулю нужно ядро новее, чем будет на сервере: сначала обновляется ядро.
|
|
st.Blocked = "нужно ядро не ниже " + a.MinCore + " (будет " + coreAfter + ") — сначала обновите ядро"
|
|
st.Update = false
|
|
}
|
|
}
|
|
}
|
|
out = append(out, st)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// MarkSplitModules — отметить к установке выделенные из ядра модули (FromCore), которых ещё нет на
|
|
// этом сервере, если они есть в релизе и разрешены лицензией (allowed — по признаку компонента).
|
|
func MarkSplitModules(states []ComponentState, allowed func(feature string) bool) {
|
|
for i := range states {
|
|
s := &states[i]
|
|
if s.FromCore && !s.Present && s.Available != "" && s.Blocked == "" && allowed(s.Feature) {
|
|
s.Update, s.New = true, true
|
|
}
|
|
}
|
|
}
|
|
|
|
// cachedHash — sha256 файла с кэшем по размеру и времени изменения: страница настроек
|
|
// не пересчитывает десятки мегабайт на каждый показ.
|
|
var hashCache = struct {
|
|
sync.Mutex
|
|
m map[string]hashEntry
|
|
}{m: map[string]hashEntry{}}
|
|
|
|
type hashEntry struct {
|
|
size int64
|
|
mod time.Time
|
|
sha string
|
|
}
|
|
|
|
func cachedHash(path string, fi os.FileInfo) string {
|
|
hashCache.Lock()
|
|
e, ok := hashCache.m[path]
|
|
hashCache.Unlock()
|
|
if ok && e.size == fi.Size() && e.mod.Equal(fi.ModTime()) {
|
|
return e.sha
|
|
}
|
|
sha, _, err := HashFile(path)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
hashCache.Lock()
|
|
hashCache.m[path] = hashEntry{size: fi.Size(), mod: fi.ModTime(), sha: sha}
|
|
hashCache.Unlock()
|
|
return sha
|
|
}
|