Настройка: состояние служб ядра и модулей с перезапуском из админки
Модули работают отдельными службами (bj-npr, bj-quikexport, bj-lk), и когда служба лежит, раздел модуля отвечает ошибкой связи, а администратору предлагалось идти в консоль за systemctl status. Теперь в разделе «Обновления» карточка «Службы»: состояние каждой службы, с какого времени, причина отказа и кнопка «Перезапустить». Состояние читаем обычным systemctl show — прав не требует. Перезапуск делает root-демон bj-keymedia (тот же канал, что у установки обновлений) по закрытому списку служб: чужую службу и сам демон отсюда не тронуть. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q4YVo4UMopqT5DzicqpPjL
This commit is contained in:
@@ -93,6 +93,7 @@ func registerSetup(mux *http.ServeMux, a *admin, rc *RuntimeConfig, svc *Service
|
||||
mux.HandleFunc("/admin/setup/lk/relay-php.zip", h.lkPHPRelayDownload)
|
||||
mux.HandleFunc("/admin/setup/lk/preview", h.lkPreviewSave)
|
||||
mux.HandleFunc("/admin/setup/lk/data", h.lkDataSave)
|
||||
mux.HandleFunc("/admin/setup/units/restart", h.unitRestart)
|
||||
mux.HandleFunc("/admin/setup/lk-integration", h.saveLKIntegration)
|
||||
mux.HandleFunc("/admin/setup/lk-check", h.checkLKConn)
|
||||
mux.HandleFunc("/admin/setup/lk-simulate", h.lkSimulate)
|
||||
@@ -324,6 +325,7 @@ type SetupData struct {
|
||||
LKUsersError string
|
||||
LKPreview LKPreviewStatus // кабинет на этом сервере для просмотра
|
||||
LKData LKDataView // курсы валют и ежедневный срез стоимости
|
||||
Units []UnitView // службы bj и её модулей
|
||||
Versions SystemVersions // версии компонентов bj и внешних систем
|
||||
Modules map[string]bool // какие лицензируемые модули разрешены (ключ = признак)
|
||||
Features []LicenseFeatureView // признаки лицензии для карточки
|
||||
@@ -391,6 +393,7 @@ func (h *setupHandlers) renderSetup(w http.ResponseWriter, req *http.Request, fl
|
||||
LKAgent: lkAgentStatus(s),
|
||||
LKPreview: lkPreviewStatus(h.rc, req),
|
||||
LKData: lkDataView(h.rc),
|
||||
Units: unitViews(h.rc),
|
||||
LKKeyMask: lkKeyForPage(s.LK.APIKey),
|
||||
Versions: systemVersions(h.rc),
|
||||
Modules: modulesAllowed(h.rc),
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
package lkgateway
|
||||
|
||||
// units.go — состояние служб bj и её модулей для страницы настроек.
|
||||
//
|
||||
// Модули вынесены в отдельные службы (bj-npr, bj-quikexport, bj-lk), и когда одна
|
||||
// из них лежит, раздел модуля в bj отвечает ошибкой связи. Раньше администратору
|
||||
// оставалось идти в консоль за systemctl status; здесь он видит состояние всех
|
||||
// служб и может перезапустить нужную. Читаем состояние обычным systemctl show
|
||||
// (прав не требует), перезапуск делает root-демон bj-keymedia по закрытому списку.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/release"
|
||||
)
|
||||
|
||||
// UnitView — служба для карточки настроек.
|
||||
type UnitView struct {
|
||||
Unit string // bj-npr
|
||||
Title string // Риск-монитор НПР / УДС
|
||||
Core bool // ядро (перезапуск рвёт текущую страницу)
|
||||
State string // active | inactive | failed | unknown
|
||||
Sub string // running, dead, …
|
||||
Since string // с какого времени в этом состоянии
|
||||
Result string // причина отказа (systemd Result)
|
||||
Present bool // компонент установлен на этом сервере
|
||||
}
|
||||
|
||||
// OK — служба работает.
|
||||
func (u UnitView) OK() bool { return u.State == "active" && u.Sub == "running" }
|
||||
|
||||
// unitState — состояние одной службы (без прав root).
|
||||
func unitState(unit string) UnitView {
|
||||
v := UnitView{Unit: unit, State: "unknown"}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
out, err := exec.CommandContext(ctx, "systemctl", "show", unit,
|
||||
"--property=ActiveState,SubState,ActiveEnterTimestamp,Result").Output()
|
||||
if err != nil {
|
||||
return v
|
||||
}
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
k, val, ok := strings.Cut(strings.TrimSpace(line), "=")
|
||||
if !ok || val == "" {
|
||||
continue
|
||||
}
|
||||
switch k {
|
||||
case "ActiveState":
|
||||
v.State = val
|
||||
case "SubState":
|
||||
v.Sub = val
|
||||
case "Result":
|
||||
if val != "success" {
|
||||
v.Result = val
|
||||
}
|
||||
case "ActiveEnterTimestamp":
|
||||
// «Wed 2026-09-16 01:13:56 MSK» — оставляем дату и время
|
||||
if f := strings.Fields(val); len(f) >= 3 {
|
||||
v.Since = f[1] + " " + f[2]
|
||||
}
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// unitViews — службы bj на этом сервере: ядро и установленные модули.
|
||||
func unitViews(rc *RuntimeConfig) []UnitView {
|
||||
installed := release.LoadInstalled(release.InstalledFile)
|
||||
var out []UnitView
|
||||
for _, c := range release.Components {
|
||||
if c.Unit == "" {
|
||||
continue
|
||||
}
|
||||
_, present := installed[c.Name]
|
||||
if !present && !c.Core {
|
||||
continue // модуля на сервере нет — и службы быть не должно
|
||||
}
|
||||
v := unitState(c.Unit)
|
||||
v.Title, v.Core, v.Present = c.Title, c.Core, true
|
||||
out = append(out, v)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// restartUnit — перезапуск службы через root-демон (см. bj-keymedia-worker).
|
||||
func restartUnit(ctx context.Context, unit string) (string, error) {
|
||||
cctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
|
||||
defer cancel()
|
||||
res, err := runKeymediaOp(cctx, "unit-restart", map[string]string{"extra": unit})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if s, ok := res["output"].(string); ok && s != "" {
|
||||
return s, nil
|
||||
}
|
||||
return "перезапущено: " + unit, nil
|
||||
}
|
||||
|
||||
// unitRestart — POST /admin/setup/units/restart: перезапуск службы из настроек.
|
||||
func (h *setupHandlers) unitRestart(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
unit := strings.TrimSpace(r.FormValue("unit"))
|
||||
known := false
|
||||
for _, c := range release.Components {
|
||||
if c.Unit == unit {
|
||||
known = true
|
||||
}
|
||||
}
|
||||
if !known {
|
||||
setupFlash(w, r, "Неизвестная служба.")
|
||||
return
|
||||
}
|
||||
msg, err := restartUnit(r.Context(), unit)
|
||||
if err != nil {
|
||||
setupFlash(w, r, "Перезапуск "+unit+": "+err.Error())
|
||||
return
|
||||
}
|
||||
setupFlash(w, r, msg)
|
||||
}
|
||||
@@ -1073,6 +1073,22 @@ function hbBind(inp,box,list,onPick){
|
||||
<h1>Обновления</h1>
|
||||
{{template "verline" (.Versions.Get "bj-server")}}
|
||||
|
||||
<div class="card" id="sec-units">
|
||||
<h2><span class="dot {{$bad := 0}}{{range .Units}}{{if not .OK}}{{$bad = 1}}{{end}}{{end}}{{if eq $bad 1}}err{{else}}ok{{end}}"></span>Службы</h2>
|
||||
<p class="muted">Ядро и модули работают отдельными службами: если служба модуля не запущена, его раздел в bj отвечает ошибкой связи. Здесь видно состояние и можно перезапустить службу, не заходя на сервер.</p>
|
||||
<div class="tw" style="margin-top:8px"><table>
|
||||
<thead><tr><th>Служба</th><th>Что делает</th><th>Состояние</th><th>С</th><th></th></tr></thead>
|
||||
<tbody>{{range .Units}}<tr>
|
||||
<td><code>{{.Unit}}</code></td>
|
||||
<td>{{.Title}}</td>
|
||||
<td>{{if .OK}}<span class="badge ok">работает</span>{{else}}<span class="badge err">{{.State}}{{with .Sub}} / {{.}}{{end}}{{with .Result}} ({{.}}){{end}}</span>{{end}}</td>
|
||||
<td class="muted">{{if .Since}}{{.Since}}{{else}}—{{end}}</td>
|
||||
<td style="white-space:nowrap"><form method="post" action="/admin/setup/units/restart" style="margin:0" onsubmit="return confirm('{{if .Core}}Перезапустить ядро? Страница настроек на несколько секунд станет недоступна.{{else}}Перезапустить службу {{.Unit}}?{{end}}');">
|
||||
<input type="hidden" name="section" value="sec-units"><input type="hidden" name="unit" value="{{.Unit}}">
|
||||
<button type="submit" class="btn btn-secondary">Перезапустить</button></form></td>
|
||||
</tr>{{end}}</tbody></table></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Компоненты</h2>
|
||||
<p class="muted">Каждый компонент обновляется отдельно: своя версия в релизе, перезапускается только его служба. Файлы ставит root-установщик после повторной проверки подписи и контрольных сумм. Модули, которых нет на этом сервере, обновлением не ставятся — кроме выделенных из ядра (НПР / УДС, экспорт QUIK): если модуль входит в лицензию, он ставится вместе с обновлением, иначе его раздел перестал бы работать.</p>
|
||||
|
||||
Reference in New Issue
Block a user