Обновления из файла для закрытого контура: пакет с портала → проверка подписи → ядро, потом модули
- 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
This commit is contained in:
@@ -78,6 +78,7 @@ func registerSetup(mux *http.ServeMux, a *admin, rc *RuntimeConfig, svc *Service
|
||||
mux.HandleFunc("/admin/setup/update", h.saveUpdate)
|
||||
mux.HandleFunc("/admin/setup/update/check", h.checkUpdate)
|
||||
mux.HandleFunc("/admin/setup/update/apply", h.applyUpdate)
|
||||
mux.HandleFunc("/admin/setup/update/upload", h.uploadUpdate)
|
||||
mux.HandleFunc("/admin/setup/license", h.saveLicense)
|
||||
mux.HandleFunc("/admin/setup/license/activate", h.licenseActivate)
|
||||
mux.HandleFunc("/admin/setup/license/renew", h.licenseRenewNow)
|
||||
@@ -1287,6 +1288,53 @@ func (h *setupHandlers) applyUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
setupFlash(w, r, "Обновление "+what+" запущено: файлы скачиваются и проверяются, затем перезапускаются службы. Итог — в строке «Последняя проверка» через минуту; при обновлении ядра страница временно не отвечает.")
|
||||
}
|
||||
|
||||
// uploadUpdate — POST /admin/setup/update/upload: пакет обновления файлом (закрытый контур).
|
||||
// Файл сохраняется в каталог подготовки, дальше — в фоне, как и обновление по сети.
|
||||
func (h *setupHandlers) uploadUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
r.Body = http.MaxBytesReader(w, r.Body, 2<<30)
|
||||
f, hdr, err := r.FormFile("bundle")
|
||||
if err != nil {
|
||||
setupFlash(w, r, "Обновление из файла: файл не получен ("+err.Error()+")")
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
if err := os.MkdirAll(updateStagingDir, 0o750); err != nil {
|
||||
setupFlash(w, r, "Обновление из файла: каталог подготовки: "+err.Error())
|
||||
return
|
||||
}
|
||||
dst := filepath.Join(updateStagingDir, fmt.Sprintf("upload-%d.zip", time.Now().UnixNano()))
|
||||
out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o640)
|
||||
if err != nil {
|
||||
setupFlash(w, r, "Обновление из файла: "+err.Error())
|
||||
return
|
||||
}
|
||||
n, err := io.Copy(out, f)
|
||||
out.Close()
|
||||
if err != nil {
|
||||
_ = os.Remove(dst)
|
||||
setupFlash(w, r, "Обновление из файла: не удалось сохранить: "+err.Error())
|
||||
return
|
||||
}
|
||||
rc := h.rc
|
||||
go func() {
|
||||
msg, err := NewUpdater(rc).ApplyBundle(context.Background(), dst)
|
||||
cfg := rc.Snapshot().Update
|
||||
if err != nil {
|
||||
msg = "Обновление из файла не применено: " + err.Error()
|
||||
} else {
|
||||
msg = "Обновление: " + msg
|
||||
}
|
||||
log.Printf("lk-gateway: %s", msg)
|
||||
cfg.LastResult, cfg.LastCheck = msg, time.Now().UTC()
|
||||
_ = rc.SaveUpdateSettings(cfg)
|
||||
}()
|
||||
setupFlash(w, r, fmt.Sprintf("Файл %s (%d МБ) принят: проверяется подпись, затем ставятся компоненты новее текущих — ядро, потом модули. Итог — в строке «Последняя проверка» через минуту; при обновлении ядра страница временно не отвечает.", hdr.Filename, n>>20))
|
||||
}
|
||||
|
||||
// runTestClaim делает CreateClaim + ждёт финального состояния через GetClaim.
|
||||
func (h *setupHandlers) runTestClaim() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
|
||||
@@ -11,6 +11,7 @@ package lkgateway
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
@@ -356,3 +357,106 @@ func (u *Updater) updateLoop(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ApplyBundle — обновление из файла для закрытого контура: пакет с портала клиента
|
||||
// (подписанный манифест + файлы компонентов). Что ставить, решает тот же план, что и при
|
||||
// обновлении по сети: всё, что новее установленного, в порядке компонентов — ядро первым,
|
||||
// модули за ним. Модуль, которому нужно более новое ядро, чем будет после установки,
|
||||
// пропускается с объяснением.
|
||||
func (u *Updater) ApplyBundle(ctx context.Context, zipPath string) (string, error) {
|
||||
defer os.Remove(zipPath)
|
||||
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)
|
||||
}
|
||||
}
|
||||
pubB64 := u.rc.Snapshot().Update.PublicKey
|
||||
if pubB64 == "" {
|
||||
pubB64 = DefaultUpdatePublicKey
|
||||
}
|
||||
pub, err := release.ParsePublicKey(pubB64)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("открытый ключ подписи релизов: %w", err)
|
||||
}
|
||||
cleanupStaging(24 * time.Hour)
|
||||
staging := filepath.Join(updateStagingDir, fmt.Sprintf("%d", time.Now().UnixNano()))
|
||||
files, err := release.ExtractBundle(zipPath, staging)
|
||||
if err != nil {
|
||||
_ = os.RemoveAll(staging)
|
||||
return "", err
|
||||
}
|
||||
sm, m, err := release.ReadStagedManifest(staging, pub)
|
||||
if err != nil {
|
||||
_ = os.RemoveAll(staging)
|
||||
return "", err
|
||||
}
|
||||
_ = sm
|
||||
have := map[string]bool{}
|
||||
for _, f := range files {
|
||||
have[f] = true
|
||||
}
|
||||
plan := release.Plan("", m, release.LoadInstalled(release.InstalledFile), BuildVersion)
|
||||
release.MarkSplitModules(plan, u.allowed)
|
||||
var req struct {
|
||||
Names []string `json:"names"`
|
||||
New []string `json:"new,omitempty"`
|
||||
}
|
||||
var list, skipped []string
|
||||
for _, c := range plan {
|
||||
if c.Blocked != "" {
|
||||
skipped = append(skipped, c.Title+": "+c.Blocked)
|
||||
continue
|
||||
}
|
||||
if !c.Update {
|
||||
continue
|
||||
}
|
||||
if !have[c.Name] { // в пакете может не быть файла модуля (не входит в лицензию клиента)
|
||||
continue
|
||||
}
|
||||
if !u.allowed(c.Feature) && c.Feature != "updates" {
|
||||
skipped = append(skipped, c.Title+": не входит в лицензию")
|
||||
continue
|
||||
}
|
||||
req.Names = append(req.Names, c.Name)
|
||||
if c.New {
|
||||
req.New = append(req.New, c.Name)
|
||||
}
|
||||
list = append(list, c.Name+" "+c.Available)
|
||||
}
|
||||
if len(req.Names) == 0 {
|
||||
_ = os.RemoveAll(staging)
|
||||
msg := fmt.Sprintf("в пакете релиз %s: нечего обновлять, все компоненты актуальны", m.Version)
|
||||
if len(skipped) > 0 {
|
||||
msg += "; пропущено: " + strings.Join(skipped, "; ")
|
||||
}
|
||||
return "", errors.New(msg)
|
||||
}
|
||||
rb, _ := json.Marshal(req)
|
||||
if err := os.WriteFile(filepath.Join(staging, "request.json"), rb, 0o640); err != nil {
|
||||
return "", err
|
||||
}
|
||||
log.Printf("lk-gateway: обновление из файла (релиз %s): %s — передано root-установщику", m.Version, strings.Join(list, ", "))
|
||||
wctx, cancel := context.WithTimeout(ctx, 10*time.Minute)
|
||||
defer cancel()
|
||||
res, err := runKeymediaOp(wctx, "component-install", map[string]string{"staging": staging})
|
||||
_ = os.RemoveAll(staging)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("установка: %w", err)
|
||||
}
|
||||
msg := fmt.Sprintf("из файла (релиз %s) установлено: %s", m.Version, strings.Join(list, ", "))
|
||||
if r, ok := res["restarted"].([]any); ok && len(r) > 0 {
|
||||
var units []string
|
||||
for _, x := range r {
|
||||
units = append(units, fmt.Sprint(x))
|
||||
}
|
||||
msg += "; перезапущены службы: " + strings.Join(units, ", ")
|
||||
}
|
||||
if len(skipped) > 0 {
|
||||
msg += "; пропущено: " + strings.Join(skipped, "; ")
|
||||
}
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
@@ -1020,7 +1020,7 @@ function hbBind(inp,box,list,onPick){
|
||||
{{range .Components}}<tr>
|
||||
<td><strong>{{.Title}}</strong><div class="muted" style="font-size:11px"><code>{{.Name}}</code></div></td>
|
||||
<td>{{if .Present}}{{if .Installed}}{{.Installed}}{{else}}<span class="muted">неизвестна</span>{{end}}{{else}}<span class="muted">не установлен</span>{{end}}</td>
|
||||
<td>{{if .Available}}{{.Available}}{{if .New}} <span class="hero-status warn">к установке</span>{{else if .Update}} <span class="hero-status warn">новее</span>{{end}}{{else}}<span class="muted">нет данных</span>{{end}}</td>
|
||||
<td>{{if .Available}}{{.Available}}{{if .New}} <span class="hero-status warn">к установке</span>{{else if .Update}} <span class="hero-status warn">новее</span>{{end}}{{if .Blocked}}<div class="muted" style="font-size:11px;color:var(--warn)">{{.Blocked}}</div>{{end}}{{else}}<span class="muted">нет данных</span>{{end}}</td>
|
||||
<td><code style="font-size:11px">{{.Unit}}</code></td>
|
||||
<td>{{if .Update}}<form method="post" action="/admin/setup/update/apply" style="margin:0" onsubmit="return confirm('{{if .New}}Установить модуль {{.Available}}? Будет включена его служба.{{else}}Обновить компонент до {{.Available}}? Его служба перезапустится.{{end}}');"><input type="hidden" name="section" value="sec-update"><input type="hidden" name="component" value="{{.Name}}"><button type="submit" class="btn btn-ok">{{if .New}}Установить{{else}}Обновить{{end}}</button></form>{{end}}</td>
|
||||
</tr>{{end}}
|
||||
@@ -1029,8 +1029,14 @@ function hbBind(inp,box,list,onPick){
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;margin-top:12px">
|
||||
{{if .UpdatesAvailable}}<form method="post" action="/admin/setup/update/apply" style="margin:0" onsubmit="return confirm('Обновить все компоненты, для которых есть новая версия?');"><input type="hidden" name="section" value="sec-update"><button type="submit" class="btn btn-ok">Обновить всё</button></form>{{end}}
|
||||
<form method="post" action="/admin/setup/update/check" style="margin:0"><input type="hidden" name="section" value="sec-update"><button type="submit" class="btn btn-secondary">Проверить обновления</button></form>
|
||||
<form method="post" action="/admin/setup/update/upload" enctype="multipart/form-data" style="margin:0;display:flex;gap:8px;align-items:center;flex-wrap:wrap" onsubmit="return this.bundle.files.length>0 && confirm('Установить обновление из файла? Подпись пакета будет проверена; поставятся компоненты новее текущих — сначала ядро, затем модули. Службы перезапустятся.');">
|
||||
<input type="hidden" name="section" value="sec-update">
|
||||
<input type="file" name="bundle" accept=".zip,application/zip" required style="max-width:260px">
|
||||
<button type="submit" class="btn btn-secondary" title="Пакет обновления с портала клиента — для сервера без выхода к серверу обновлений">Обновить из файла</button>
|
||||
</form>
|
||||
</div>
|
||||
{{if .Settings.Update.Notes}}<p class="muted" style="margin-top:10px">Что нового в релизе {{.Settings.Update.Available}}: {{.Settings.Update.Notes}}</p>{{end}}
|
||||
<p class="muted" style="margin-top:8px;font-size:12px"><b>Закрытый контур.</b> Если у сервера нет выхода к серверу обновлений, скачайте пакет обновления на портале клиента (lic.zetit.ru → Установка → «Пакет обновления») и загрузите его кнопкой «Обновить из файла». Система сама распознаёт, что внутри — ядро, модули или всё вместе — и ставит в правильном порядке. Модуль, которому нужно более новое ядро, чем установлено, будет пропущен с подсказкой обновить сначала ядро.</p>
|
||||
{{if not .Settings.Update.LastCheck.IsZero}}<p class="muted" style="margin-top:6px;font-size:12px">Последняя проверка: {{.Settings.Update.LastCheck.Format "02.01.2006 15:04"}}. {{.Settings.Update.LastResult}}</p>{{end}}
|
||||
{{if and .License.Present (not .License.AllowsUpdates)}}<p class="muted" style="margin-top:6px;font-size:12px;color:var(--warn)">Текущий план «{{.License.Plan}}» не включает обновления.</p>{{end}}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user