Проверка на чистой ВМ: служба впервые установленного модуля включается в автозапуск, даже если unit-файл уже положил образ (иначе bj-crypto после перезагрузки не стартовал); bj-crypto в готовности — предупреждение, а не блокер (пакеты для НРД подписывает шлюз), до установки модуля не проверяется; в мастере без ссылки «Открыть мастер» на самом мастере, без двойного заголовка «Наша организация», без QUIK и DEPO в текстах и маршрутах уведомлений, если их нет в лицензии; «162 участника», понятная сводка готовности; адрес SMTP через JoinHostPort
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
@@ -833,13 +833,13 @@ func nowPage(title, active string) page {
|
||||
switch {
|
||||
case s.NSD.IGWBaseURL == "":
|
||||
p.IsMockMode = true
|
||||
p.MockReason = "ИШ НРД не настроен — обмен с НРД не выполняется"
|
||||
p.MockReason = "Не подключён интеграционный шлюз НРД."
|
||||
if p.DevMode {
|
||||
p.MockReason = "ИШ НРД не настроен — заявки идут через внутренний mock (Decision эмитируется через 3 сек)"
|
||||
}
|
||||
case s.Crypto.Provider == "" || s.Crypto.Provider == "stub":
|
||||
p.IsMockMode = true
|
||||
p.MockReason = "Не настроено средство подписи (СКЗИ) — обмен с НРД невозможен"
|
||||
p.MockReason = "Не настроено средство подписи (СКЗИ)."
|
||||
if p.DevMode {
|
||||
p.MockReason = "Провайдер СКЗИ = stub — подпись не делается, реальный обмен с НРД невозможен"
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"mime"
|
||||
"net"
|
||||
"net/smtp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -64,7 +65,7 @@ func sendSMTP(s SMTPSettings, subject, body string) error {
|
||||
if port == 0 {
|
||||
port = 587
|
||||
}
|
||||
addr := fmt.Sprintf("%s:%d", s.Host, port)
|
||||
addr := net.JoinHostPort(s.Host, strconv.Itoa(port))
|
||||
msg := buildMIME(s.From, rcpts, subject, body)
|
||||
|
||||
dialer := &net.Dialer{Timeout: 15 * time.Second}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -236,3 +237,33 @@ func TestLettersFill(t *testing.T) {
|
||||
t.Error("неизвестное письмо")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuPlural(t *testing.T) {
|
||||
for n, want := range map[int]string{1: "участник", 2: "участника", 4: "участника", 5: "участников", 11: "участников", 12: "участников", 21: "участник", 162: "участника", 111: "участников", 0: "участников"} {
|
||||
if got := ruPlural(n, "участник", "участника", "участников"); got != want {
|
||||
t.Errorf("%d: %q, ожидалось %q", n, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Без лицензии QUIK МНП маршрута «ошибки QUIK» в форме нет, а сохранение формы не стирает его прежнее значение.
|
||||
func TestNotifyKindsByLicense(t *testing.T) {
|
||||
rc, _ := NewRuntimeConfig(t.TempDir() + "/setup.json")
|
||||
for _, k := range visibleNotifyKinds(rc) {
|
||||
if k.Key == "quik" {
|
||||
t.Fatal("без лицензии QUIK МНП маршрута QUIK быть не должно")
|
||||
}
|
||||
}
|
||||
if len(visibleNotifyKinds(rc)) != len(notifyKinds)-1 {
|
||||
t.Fatalf("типы: %+v", visibleNotifyKinds(rc))
|
||||
}
|
||||
h := &setupHandlers{rc: rc}
|
||||
form := url.Values{"enabled": {"on"}, "emails": {"depo@example.ru"}, "route_incoming_depo": {"1"}}
|
||||
req := httptest.NewRequest(http.MethodPost, "/admin/setup/incoming-notify", strings.NewReader(form.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
h.saveIncomingNotify(httptest.NewRecorder(), req)
|
||||
r := rc.Snapshot().Incoming.Routes
|
||||
if r["incoming"] != "depo" || r["quik"] != defaultNotifyRoutes["quik"] {
|
||||
t.Fatalf("маршруты после сохранения: %+v", r)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -18,6 +19,23 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// cryptoJarPath — модуль службы пробной подписи bj-crypto (компонент crypto-service.jar).
|
||||
const cryptoJarPath = "/opt/bj/crypto-service.jar"
|
||||
|
||||
// ruPlural — форма слова для числа: 1 участник, 2 участника, 5 участников.
|
||||
func ruPlural(n int, one, few, many string) string {
|
||||
n %= 100
|
||||
switch {
|
||||
case n >= 11 && n <= 14:
|
||||
return many
|
||||
case n%10 == 1:
|
||||
return one
|
||||
case n%10 >= 2 && n%10 <= 4:
|
||||
return few
|
||||
}
|
||||
return many
|
||||
}
|
||||
|
||||
// ReadyCheck — одна строка итоговой проверки.
|
||||
type ReadyCheck struct {
|
||||
Order int `json:"-"`
|
||||
@@ -107,16 +125,22 @@ func readinessChecks(ctx context.Context, rc *RuntimeConfig) []ReadyCheck {
|
||||
cctx, cancel := context.WithTimeout(ctx, 4*time.Second)
|
||||
defer cancel()
|
||||
ver, _, _ := validataVersion(cctx)
|
||||
out, _ := exec.CommandContext(cctx, "systemctl", "is-active", "bj-crypto.service").Output()
|
||||
svc := strings.TrimSpace(string(out))
|
||||
c := ReadyCheck{Order: 6, Title: "СКЗИ «Валидата» и служба криптографии", Link: "/admin/wizard?step=6", Required: true}
|
||||
switch {
|
||||
case ver == "":
|
||||
c.State, c.Detail = "err", "Валидата на сервере не найдена"
|
||||
case svc != "active":
|
||||
c.State, c.Detail = "err", "Валидата "+ver+", но служба bj-crypto не запущена ("+pickStr(svc, "нет")+")"
|
||||
default:
|
||||
c.State, c.Detail = "ok", "Валидата "+ver+", служба bj-crypto работает"
|
||||
c := ReadyCheck{Order: 6, Title: "СКЗИ «Валидата»", Link: "/admin/wizard?step=6", Required: true}
|
||||
if ver == "" {
|
||||
c.State, c.Detail = "err", "Валидата на сервере не найдена — без неё шлюз не подпишет пакеты для НРД"
|
||||
return c
|
||||
}
|
||||
c.State, c.Detail = "ok", "Валидата "+ver
|
||||
// Служба bj-crypto — пробная подпись и проверка ключа из bj; пакеты для НРД подписывает шлюз, поэтому
|
||||
// её остановка обмену не мешает. Модуль приезжает с обновлениями — до них службы нет, это не ошибка.
|
||||
if _, err := os.Stat(cryptoJarPath); err == nil {
|
||||
out, _ := exec.CommandContext(cctx, "systemctl", "is-active", "bj-crypto.service").Output()
|
||||
if svc := strings.TrimSpace(string(out)); svc != "active" {
|
||||
c.State, c.Link = "warn", "/admin/setup#sec-crypto"
|
||||
c.Detail += "; служба пробной подписи bj-crypto не запущена (" + pickStr(svc, "нет") + ") — обмену не мешает: пакеты для НРД подписывает шлюз"
|
||||
} else {
|
||||
c.Detail += ", служба пробной подписи bj-crypto работает"
|
||||
}
|
||||
}
|
||||
return c
|
||||
})
|
||||
@@ -176,7 +200,11 @@ func readinessChecks(ctx context.Context, rc *RuntimeConfig) []ReadyCheck {
|
||||
case s.Alerts.Zabbix.Enabled:
|
||||
c.State, c.Detail = "ok", "Zabbix "+s.Alerts.Zabbix.Server
|
||||
default:
|
||||
c.State, c.Detail = "warn", "не настроены: о сбоях (НРД не отвечает, недоставленные переводы, ошибки DEPO, срок сертификата) узнают только открыв bj"
|
||||
depo := ""
|
||||
if moduleAllowed(rc, "quik-mnp") {
|
||||
depo = ", ошибки DEPO"
|
||||
}
|
||||
c.State, c.Detail = "warn", "не настроены: о сбоях (НРД не отвечает, недоставленные переводы"+depo+", срок сертификата) узнают только открыв bj"
|
||||
}
|
||||
return c
|
||||
})
|
||||
@@ -241,9 +269,9 @@ func readinessChecks(ctx context.Context, rc *RuntimeConfig) []ReadyCheck {
|
||||
case len(hb.Participants) == 0:
|
||||
c.State, c.Detail = "err", "справочника нет — получателя перевода не определить"
|
||||
case !at.IsZero() && time.Since(at) > 14*24*time.Hour:
|
||||
c.State, c.Detail = "warn", fmt.Sprintf("%d участников, от %s — давно не обновлялся (обновляется по связи с НРД)", len(hb.Participants), at.Format("02.01.2006"))
|
||||
c.State, c.Detail = "warn", fmt.Sprintf("%d %s, от %s — давно не обновлялся (обновляется по связи с НРД)", len(hb.Participants), ruPlural(len(hb.Participants), "участник", "участника", "участников"), at.Format("02.01.2006"))
|
||||
default:
|
||||
c.State, c.Detail = "ok", fmt.Sprintf("%d участников, от %s", len(hb.Participants), at.Format("02.01.2006"))
|
||||
c.State, c.Detail = "ok", fmt.Sprintf("%d %s, от %s", len(hb.Participants), ruPlural(len(hb.Participants), "участник", "участника", "участников"), at.Format("02.01.2006"))
|
||||
}
|
||||
return c
|
||||
})
|
||||
|
||||
@@ -602,6 +602,18 @@ var notifyKinds = []struct{ Key, Title string }{
|
||||
{"system", "Прочие системные оповещения"},
|
||||
}
|
||||
|
||||
// visibleNotifyKinds — типы уведомлений этой установки: ошибки QUIK МНП — только при модуле в лицензии.
|
||||
func visibleNotifyKinds(rc *RuntimeConfig) []struct{ Key, Title string } {
|
||||
var out []struct{ Key, Title string }
|
||||
for _, k := range notifyKinds {
|
||||
if k.Key == "quik" && rc != nil && !moduleAllowed(rc, "quik-mnp") {
|
||||
continue
|
||||
}
|
||||
out = append(out, k)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// defaultNotifyRoutes — маршруты по умолчанию, пока оператор не настроил свои.
|
||||
var defaultNotifyRoutes = map[string]string{"incoming": "depo,back", "orders": "depo,back", "quik": "back,middle", "system": "back"}
|
||||
|
||||
|
||||
@@ -311,6 +311,7 @@ func (h *setupHandlers) renderWizard(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
if (step >= 3 && step <= 5) || step == 9 || step == 11 { // DEPO, организация, QUIK МНП, оповещения и копии, модули — общие блоки из «Настройки»
|
||||
fd := h.setupData(w, r, "")
|
||||
fd.InWizard = true
|
||||
d.Fancy = &fd
|
||||
}
|
||||
if step == 3 {
|
||||
@@ -458,6 +459,7 @@ func (h *setupHandlers) testSignCrypto(w http.ResponseWriter, r *http.Request) {
|
||||
type SetupData struct {
|
||||
page
|
||||
FromWizard bool // раздел открыт из мастера (настройка модуля) — показать «Вернуться в мастер»
|
||||
InWizard bool // общий блок показан внутри шага мастера — заголовок и пояснение даёт сам шаг
|
||||
Settings Settings
|
||||
Suggest *DepoRequisites // предложение реквизитов из DEPO
|
||||
Readiness []Readiness
|
||||
@@ -554,7 +556,7 @@ func (h *setupHandlers) setupData(w http.ResponseWriter, req *http.Request, flas
|
||||
OrgName: orgName(s),
|
||||
OrgCode: orgCode(s),
|
||||
NotifyGroups: notifyGroups,
|
||||
NotifyKinds: notifyKinds,
|
||||
NotifyKinds: visibleNotifyKinds(h.rc),
|
||||
DepoJob: depoJobView(s.Fancy),
|
||||
DepoTables: depoTablesView(s.Fancy),
|
||||
QuikExport: quikExportView(h.rc, req.URL.Query().Get("reveal_ro") == "1"),
|
||||
@@ -1103,7 +1105,14 @@ func (h *setupHandlers) saveIncomingNotify(w http.ResponseWriter, r *http.Reques
|
||||
EmailsMiddle: strings.TrimSpace(r.FormValue("emails_middle")),
|
||||
Routes: map[string]string{},
|
||||
}
|
||||
for _, k := range notifyKinds {
|
||||
prev := h.rc.Snapshot().Incoming.Routes
|
||||
if prev == nil {
|
||||
prev = defaultNotifyRoutes
|
||||
}
|
||||
for _, k := range notifyKinds { // скрытые в форме типы (модуль не в лицензии) — прежний маршрут
|
||||
s.Routes[k.Key] = prev[k.Key]
|
||||
}
|
||||
for _, k := range visibleNotifyKinds(h.rc) {
|
||||
var gs []string
|
||||
for _, g := range notifyGroups {
|
||||
if r.FormValue("route_"+k.Key+"_"+g.Key) != "" {
|
||||
|
||||
@@ -316,8 +316,8 @@ a.wstep:hover .bub { box-shadow:0 0 0 4px var(--accent-weak); }
|
||||
{{if eq .Step 4}}
|
||||
<div class="card">
|
||||
<h2><span class="dot {{if .Done.Org}}ok{{else}}err{{end}}"></span>Наша организация</h2>
|
||||
<div class="help-block"><strong>Что это.</strong> Депозитарий — участник MOEX МОСТ, от имени которого bj отправляет и принимает переводы: код участника, ИНН и наименование попадают в пакеты НРД и в поручения QUIK.<br>
|
||||
<strong>Откуда взять.</strong> Если подключена база DEPO (раздел «Учёт DEPO»), bj сам определяет реквизиты по учёту — проверьте их и сохраните. Иначе выберите себя из справочника участников НРД или введите вручную.</div>
|
||||
<div class="help-block"><strong>Что это.</strong> Депозитарий — участник MOEX МОСТ, от имени которого bj отправляет и принимает переводы: код участника, ИНН и наименование попадают в пакеты НРД{{if .DepoLicensed}} и в поручения QUIK{{end}}.<br>
|
||||
<strong>Откуда взять.</strong> {{if .DepoLicensed}}Если подключена база DEPO (раздел «Учёт DEPO»), bj сам определяет реквизиты по учёту — проверьте их и сохраните. Иначе выберите{{else}}Выберите{{end}} себя из справочника участников НРД или введите вручную.</div>
|
||||
{{if .Suggest}}{{if .Suggest.OrgName}}{{if not .Done.Org}}<p style="margin:8px 0;padding:8px 12px;border-radius:8px;background:var(--accent-weak)">✓ <strong>Определено из DEPO</strong>: {{.Suggest.OrgName}}, ИНН {{.Suggest.OrgINN}}, код {{.Suggest.OrgCode}}{{if .Suggest.AccountID}}, счёт НРД {{.Suggest.AccountID}}{{end}}. Поля ниже заполнены — проверьте и нажмите «Сохранить».</p>{{end}}{{end}}{{end}}
|
||||
{{with .Fancy}}{{template "orgSection" .}}{{end}}
|
||||
<div class="card" id="nsd-req" style="margin-top:12px">
|
||||
@@ -517,7 +517,7 @@ a.wstep:hover .bub { box-shadow:0 0 0 4px var(--accent-weak); }
|
||||
{{if eq .Step 9}}
|
||||
<div class="card">
|
||||
<h2><span class="dot {{if .Done.Alerts}}ok{{else}}warn{{end}}"></span>Оповещения и резервные копии</h2>
|
||||
<div class="help-block"><strong>Зачем.</strong> bj сам следит за обменом: НРД не отвечает, перевод не доставлен, ошибка обработки в DEPO, истекает сертификат ключа. Чтобы об этом узнали сразу, а не открыв bj, — укажите почту. Резервные копии позволят восстановить сервер целиком (настройки, заявки, шлюз НРД) после сбоя или переустановки.<br>
|
||||
<div class="help-block"><strong>Зачем.</strong> bj сам следит за обменом: НРД не отвечает, перевод не доставлен{{if .DepoLicensed}}, ошибка обработки в DEPO{{end}}, истекает сертификат ключа. Чтобы об этом узнали сразу, а не открыв bj, — укажите почту. Резервные копии позволят восстановить сервер целиком (настройки, заявки, шлюз НРД) после сбоя или переустановки.<br>
|
||||
<strong>Можно позже.</strong> Шаг необязательный — обмен работает и без него, но мы настоятельно советуем настроить оба пункта.</div>
|
||||
<style>.wiz9 label:not(:has(input[type=checkbox])){display:block;margin:0}.wiz9 label input:not([type=checkbox]),.wiz9 label select,.wiz9 .grid input,.wiz9 .grid select{width:100%;box-sizing:border-box;margin-top:4px}</style>
|
||||
<div class="wiz9">
|
||||
@@ -568,11 +568,12 @@ a.wstep:hover .bub { box-shadow:0 0 0 4px var(--accent-weak); }
|
||||
var tb=document.querySelector('#rd-tab tbody'), sum=document.getElementById('rd-sum'), dot=document.getElementById('rd-dot');
|
||||
function esc(s){return String(s).replace(/[&<>"]/g,function(c){return{'&':'&','<':'<','>':'>','"':'"'}[c]})}
|
||||
var ic={ok:'<span style="color:var(--ok)">✓</span>',warn:'<span style="color:var(--warn)">⚠</span>',err:'<span style="color:var(--err)">✗</span>',skip:'<span class="muted">—</span>'};
|
||||
function pl(n,one,few,many){var m=n%100,d=n%10;return (m>=11&&m<=14)?many:d===1?one:(d>=2&&d<=4)?few:many}
|
||||
function load(){ sum.textContent='Проверяю…'; tb.innerHTML='';
|
||||
fetch('/admin/setup/readiness',{cache:'no-store'}).then(function(r){return r.json()}).then(function(d){
|
||||
tb.innerHTML=d.checks.map(function(c){return '<tr><td style="width:24px;vertical-align:top">'+ic[c.state]+'</td><td style="vertical-align:top;white-space:nowrap;padding-right:10px"><b>'+esc(c.title)+'</b></td><td style="vertical-align:top">'+esc(c.detail)+(c.state!=='ok'&&c.link?' <a href="'+esc(c.link)+'">Исправить →</a>':'')+'</td></tr>'}).join('');
|
||||
var red=d.checks.filter(function(c){return c.state==='err'}).length, yel=d.checks.filter(function(c){return c.state==='warn'}).length;
|
||||
sum.innerHTML=d.ready?(yel?'<span style="color:var(--ok)">✓ Обмен с НРД готов к работе</span> <span class="muted" style="font-weight:400">— желательно исправить: '+yel+'</span>':'<span style="color:var(--ok)">✓ Всё готово к работе</span>'):'<span style="color:var(--err)">✗ Не готово: обязательных проблем — '+red+'</span>';
|
||||
sum.innerHTML=d.ready?(yel?'<span style="color:var(--ok)">✓ Обмен с НРД готов к работе</span> <span class="muted" style="font-weight:400">— желательно исправить ещё '+yel+' '+pl(yel,'пункт','пункта','пунктов')+' (отмечены ⚠)</span>':'<span style="color:var(--ok)">✓ Всё готово к работе</span>'):'<span style="color:var(--err)">✗ Пока не готово: осталось '+red+' '+pl(red,'обязательный пункт','обязательных пункта','обязательных пунктов')+' (отмечены ✗)</span>';
|
||||
if(dot) dot.className='dot '+(d.ready?(yel?'warn':'ok'):'err');
|
||||
document.getElementById('rd-at').textContent='проверено '+d.at;
|
||||
}).catch(function(){ sum.textContent='Проверка не выполнилась — обновите страницу.'; }); }
|
||||
|
||||
@@ -393,7 +393,7 @@ details:has(> pre.copyable) { min-width:0; max-width:100%; }
|
||||
<div class="banner-mock">
|
||||
<span class="dot warn" style="width:9px;height:9px;margin:0"></span>
|
||||
<div><strong style="color:var(--warn)">{{if devOn}}Режим эмуляции{{else}}Обмен с НРД не настроен{{end}}</strong> — {{if devOn}}реального обмена с НРД нет{{else}}поручения в НРД не отправляются{{end}}. <span class="muted">{{.MockReason}}</span></div>
|
||||
<a href="/admin/wizard" style="margin-left:auto">Открыть мастер настройки →</a>
|
||||
{{if ne .Active "wizard"}}<a href="/admin/wizard" style="margin-left:auto">Открыть мастер настройки →</a>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{{/* «Наша организация» — одна и та же в «Настройка → НРД» и в мастере настройки (данные — SetupData). */}}
|
||||
{{define "orgSection"}}
|
||||
<div class="card" id="sec-org">
|
||||
<h2><span class="dot {{if and .OrgINN .OrgCode}}ok{{else}}err{{end}}"></span>Наша организация</h2>
|
||||
<p class="muted">Депозитарий — участник MOEX МОСТ, от имени которого bj отправляет и принимает переводы: ИНН передающего и зачисляющего депозитария в пакетах, код отправителя и код фирмы в QUIK. Выберите себя из справочника участников НРД или введите данные вручную.</p>
|
||||
{{if not .InWizard}}<h2><span class="dot {{if and .OrgINN .OrgCode}}ok{{else}}err{{end}}"></span>Наша организация</h2>
|
||||
<p class="muted">Депозитарий — участник MOEX МОСТ, от имени которого bj отправляет и принимает переводы: ИНН передающего и зачисляющего депозитария в пакетах{{if mod "quik-mnp"}}, код отправителя и код фирмы в QUIK{{end}}. Выберите себя из справочника участников НРД или введите данные вручную.</p>{{end}}
|
||||
{{if and .OrgINN .OrgCode}}<p style="margin:8px 0 0"><b>{{if .OrgName}}{{.OrgName}}{{else}}—{{end}}</b> · ИНН <code>{{.OrgINN}}</code> · код <code>{{.OrgCode}}</code>{{if not .Settings.NSD.OrgINN}} <span class="muted">(ИНН взят из справочника по коду)</span>{{end}}</p>
|
||||
{{else}}<p style="margin:8px 0 0;color:var(--err)">Организация не задана — переводы не будут сформированы.</p>{{end}}
|
||||
<form method="post" action="/admin/setup/org" style="margin-top:12px;display:grid;gap:10px;max-width:680px">
|
||||
|
||||
@@ -40,6 +40,7 @@ type InstallResult struct {
|
||||
Release string `json:"release,omitempty"`
|
||||
Installed []map[string]string `json:"installed,omitempty"`
|
||||
Restarted []string `json:"restarted,omitempty"`
|
||||
Enabled []string `json:"enabled,omitempty"` // службы, включённые в автозапуск (впервые установленные модули)
|
||||
}
|
||||
|
||||
// InstallStaged — установить компоненты из каталога подготовки: manifest.json (подписанный),
|
||||
@@ -129,6 +130,10 @@ func InstallStaged(staging string, opt InstallOptions) (res InstallResult) {
|
||||
ts := time.Now().Format("20060102-150405")
|
||||
var units, newUnits []string
|
||||
for _, j := range jobs {
|
||||
// Модуля на сервере ещё не было — его службу включаем, даже если unit-файл уже стоит: установка
|
||||
// с образа кладёт unit-файлы всех служб сразу (выключенными), а файл модуля приезжает обновлением.
|
||||
_, statErr := os.Stat(j.c.PathAt(opt.Root))
|
||||
fresh := statErr != nil
|
||||
if err := installFile(j.file, j.c.PathAt(opt.Root), j.c.Exec, ts); err != nil {
|
||||
return fail("установка %s: %v", j.c.Name, err)
|
||||
}
|
||||
@@ -140,7 +145,7 @@ func InstallStaged(staging string, opt InstallOptions) (res InstallResult) {
|
||||
if err != nil {
|
||||
return fail("служба %s: %v", j.c.Unit, err)
|
||||
}
|
||||
if added {
|
||||
if added || fresh {
|
||||
newUnits = append(newUnits, j.c.Unit)
|
||||
}
|
||||
}
|
||||
@@ -153,6 +158,7 @@ func InstallStaged(staging string, opt InstallOptions) (res InstallResult) {
|
||||
return fail("включение служб %s: %v: %s", strings.Join(newUnits, ", "), err, strings.TrimSpace(string(out)))
|
||||
}
|
||||
}
|
||||
res.Enabled = newUnits
|
||||
for _, j := range jobs {
|
||||
if j.c.Name == "bj-installer" && opt.Root == "" {
|
||||
// новые службы и скрипты из обновлённого установщика; перезапуск служб — отдельно ниже
|
||||
|
||||
@@ -66,6 +66,29 @@ func TestInstallSplitModuleWithUnit(t *testing.T) {
|
||||
if inst := LoadInstalled(filepath.Join(root, InstalledFile)); inst["bj-npr"].Version != "0.1.0" {
|
||||
t.Fatalf("components.json: %+v", inst)
|
||||
}
|
||||
if len(res.Enabled) != 1 || res.Enabled[0] != "bj-npr" {
|
||||
t.Fatalf("служба впервые установленного модуля должна включаться: %+v", res.Enabled)
|
||||
}
|
||||
|
||||
// Повторная установка того же модуля: служба уже стоит и модуль был — автозапуск не трогаем
|
||||
// (администратор мог выключить службу сам).
|
||||
res = InstallStaged(stageNPR(t, priv, `{"names":["bj-npr"]}`), InstallOptions{Root: root, PubKey: pub, Units: units})
|
||||
if !res.OK || len(res.Enabled) != 0 {
|
||||
t.Fatalf("повторная установка не должна включать службу: %+v", res)
|
||||
}
|
||||
|
||||
// Установка с образа: unit-файл уже положен (выключенным), файла модуля нет — служба включается.
|
||||
root2 := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(root2, "/etc/systemd/system"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(root2, "/etc/systemd/system/bj-npr.service"), []byte("[Service]\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
res = InstallStaged(stageNPR(t, priv, `{"names":["bj-npr"],"new":["bj-npr"]}`), InstallOptions{Root: root2, PubKey: pub, Units: units})
|
||||
if !res.OK || len(res.Enabled) != 1 || res.Enabled[0] != "bj-npr" {
|
||||
t.Fatalf("unit-файл из образа: служба должна включиться при первой установке модуля: %+v", res)
|
||||
}
|
||||
|
||||
// Самостоятельный модуль (не FromCore) по new не ставится.
|
||||
c, _ := ComponentByName("bj-lk")
|
||||
|
||||
Reference in New Issue
Block a user