Ключ: два пути — «Подключить флешку с ключом» или «Сформировать ключ на новой флешке»; обновление одним заходом

- Шаг «Ключ подписи» начинается с вопроса «Где ваш ключ?»: подключить
  подготовленную флешку (bj находит на ней ключ и подключает как есть) или
  сформировать ключ на новой флешке (архив НРД и пароль → распаковка → bj ждёт
  чистую флешку; без флешки записи нет — «вставьте и обновите страницу»). Этапы
  показываются, когда путь выбран, — свои у каждого пути.
- Обновление больше не делится на два захода, когда вместе с bj обновляется и
  установщик обновлений: откладывание bj-installer нужно только установщикам
  старше 25.09.2026, которые его не знают.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
zuevav
2026-10-06 18:35:14 +03:00
parent 8071c3f9af
commit e7d81ea2a9
4 changed files with 71 additions and 32 deletions
+3 -3
View File
@@ -74,7 +74,7 @@ func defaultKeySteps() []keyWizardStep {
} }
return []keyWizardStep{ return []keyWizardStep{
{Title: "Загрузка архива и распаковка", Status: "pending"}, {Title: "Загрузка архива и распаковка", Status: "pending"},
{Title: "Запись ключа на выбранную флешку (с бэкапом)", Status: "pending"}, {Title: "Запись ключа на новую флешку (копия прежнего содержимого — на сервере)", Status: "pending"},
{Title: "Формирование справочника сертификатов (CRL)", Status: "pending"}, {Title: "Формирование справочника сертификатов (CRL)", Status: "pending"},
{Title: "Перезапуск и проверка ИШ", Status: "pending"}, {Title: "Перезапуск и проверка ИШ", Status: "pending"},
{Title: last, Status: "pending"}, {Title: last, Status: "pending"},
@@ -377,8 +377,8 @@ func (h *setupHandlers) keyWizardFromMedia(w http.ResponseWriter, r *http.Reques
h.keyWiz.StagingID, h.keyWiz.VDK = id, vdk h.keyWiz.StagingID, h.keyWiz.VDK = id, vdk
h.keyWiz.MediaDevice, h.keyWiz.MediaProfile = drive.Device, prof h.keyWiz.MediaDevice, h.keyWiz.MediaProfile = drive.Device, prof
// флешка уже подготовлена: используется как есть — этапы называются по-другому, записи на флешку нет // флешка уже подготовлена: используется как есть — этапы называются по-другому, записи на флешку нет
h.keyWiz.Steps[0].Title = "Ключ найден на подготовленной флешке" h.keyWiz.Steps[0].Title = "Поиск ключа на флешке"
h.keyWiz.Steps[1].Title = "Флешка подключается как есть — на неё ничего не записывается" h.keyWiz.Steps[1].Title = "Подключение флешки к шлюзу как есть — на неё ничего не записывается"
h.keyWiz.Steps[2].Title = "Справочник сертификатов для ИШ — на диске сервера" h.keyWiz.Steps[2].Title = "Справочник сертификатов для ИШ — на диске сервера"
h.keyWiz.set(0, "ok", fmt.Sprintf("Ключ найден на флешке %s: %s, справочник %s%s", fallback(drive.Model, drive.Device), vdk, spr, prefixed(", профиль ", prof))) h.keyWiz.set(0, "ok", fmt.Sprintf("Ключ найден на флешке %s: %s, справочник %s%s", fallback(drive.Model, drive.Device), vdk, spr, prefixed(", профиль ", prof)))
h.keyWiz.set(1, "active", "") h.keyWiz.set(1, "active", "")
+8 -1
View File
@@ -466,11 +466,18 @@ func TestDumpOnboardingPages(t *testing.T) {
// шаг «Ключ подписи»: ключ найден на подготовленной флешке — используется как есть // шаг «Ключ подписи»: ключ найден на подготовленной флешке — используется как есть
h.keyWiz = newKeyWizardState() h.keyWiz = newKeyWizardState()
h.keyWiz.StagingID, h.keyWiz.VDK, h.keyWiz.MediaDevice, h.keyWiz.MediaProfile = "media-test", "CKJCI7AYNIOSNLXK.vdk", "/dev/sdb1", "PrUser1046" h.keyWiz.StagingID, h.keyWiz.VDK, h.keyWiz.MediaDevice, h.keyWiz.MediaProfile = "media-test", "CKJCI7AYNIOSNLXK.vdk", "/dev/sdb1", "PrUser1046"
h.keyWiz.Steps[0].Title, h.keyWiz.Steps[1].Title = "Ключ найден на подготовленной флешке", "Флешка подключается как есть — на неё ничего не записывается" h.keyWiz.Steps[0].Title, h.keyWiz.Steps[1].Title = "Поиск ключа на флешке", "Подключение флешки к шлюзу как есть — на неё ничего не записывается"
h.keyWiz.set(0, "ok", "Ключ найден на флешке USB2FlashStorage: CKJCI7AYNIOSNLXK.vdk, справочник spr1046, профиль PrUser1046") h.keyWiz.set(0, "ok", "Ключ найден на флешке USB2FlashStorage: CKJCI7AYNIOSNLXK.vdk, справочник spr1046, профиль PrUser1046")
wk := httptest.NewRecorder() wk := httptest.NewRecorder()
h.renderWizard(wk, httptest.NewRequest(http.MethodGet, "/admin/wizard?step=8", nil)) h.renderWizard(wk, httptest.NewRequest(http.MethodGet, "/admin/wizard?step=8", nil))
_ = os.WriteFile(filepath.Join(dir, "wizard-08-media.html"), wk.Body.Bytes(), 0o644) _ = os.WriteFile(filepath.Join(dir, "wizard-08-media.html"), wk.Body.Bytes(), 0o644)
// путь «новая флешка»: архив распакован, ждём флешку
h.keyWiz = newKeyWizardState()
h.keyWiz.StagingID, h.keyWiz.VDK = "test-archive", "CKJCI7AYNIOSNLXK.vdk"
h.keyWiz.set(0, "ok", "Ключ CKJCI7AYNIOSNLXK.vdk, справочник spr1046")
wn := httptest.NewRecorder()
h.renderWizard(wn, httptest.NewRequest(http.MethodGet, "/admin/wizard?step=8", nil))
_ = os.WriteFile(filepath.Join(dir, "wizard-08-new.html"), wn.Body.Bytes(), 0o644)
h.keyWiz = nil h.keyWiz = nil
// шаг QUIK после проверки подключения — выбор «Как bj работает с QUIK» // шаг QUIK после проверки подключения — выбор «Как bj работает с QUIK»
q := rc.Snapshot().Quik q := rc.Snapshot().Quik
+8 -3
View File
@@ -42,6 +42,9 @@ const updateStagingDir = "/var/lib/bj/update-staging"
// DefaultUpdateURL — сервер обновлений по умолчанию (встроен в сервер лицензий). // DefaultUpdateURL — сервер обновлений по умолчанию (встроен в сервер лицензий).
const DefaultUpdateURL = "https://lic.zetit.ru/updates" const DefaultUpdateURL = "https://lic.zetit.ru/updates"
// installerKnowsAll — с этой версии root-установщик обновлений знает bj-installer и ставит выделенные модули.
const installerKnowsAll = "2026.09.25"
type Updater struct{ rc *RuntimeConfig } type Updater struct{ rc *RuntimeConfig }
func NewUpdater(rc *RuntimeConfig) *Updater { return &Updater{rc: rc} } func NewUpdater(rc *RuntimeConfig) *Updater { return &Updater{rc: rc} }
@@ -287,11 +290,13 @@ func (u *Updater) applyUpdate(ctx context.Context, names []string) (string, erro
} }
return "", fmt.Errorf("нечего обновлять: все установленные компоненты актуальны (релиз %s)", m.Version) return "", fmt.Errorf("нечего обновлять: все установленные компоненты актуальны (релиз %s)", m.Version)
} }
// Прежний root-установщик не умеет впервые ставить выделенные из ядра модули: пока он сам не // Очень старый root-установщик (до 25.09.2026) не знает bj-installer и не ставит впервые выделенные
// обновлён, такие модули откладываются — их поставит ensureSplitModules после перезапуска. // из ядра модули: пока он сам не обновлён, такие компоненты откладываются (второй заход). Установщик
// новее этого умеет всё — обновление одним заходом, даже если вместе с ним обновляется и он сам
// (раньше любое его обновление делило установку на два раза: «скачал 24 МБ — снова „Обновить“ — ещё 6»).
installerOutdated := false installerOutdated := false
for _, c := range plan { for _, c := range plan {
if c.Name == "bj-component-install" && c.Update { if c.Name == "bj-component-install" && c.Update && (c.Installed == "" || release.IsNewer(installerKnowsAll, c.Installed)) {
installerOutdated = true installerOutdated = true
} }
} }
@@ -5,7 +5,19 @@
{{define "keyWizBody"}} {{define "keyWizBody"}}
{{$s := .State}} {{$s := .State}}
{{/* ===== Лента шагов ===== */}} <style>
.kw-tiles{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin-top:6px}
.kw-tile{display:flex;gap:10px;align-items:flex-start;padding:12px 14px;border:1px solid var(--border);border-radius:12px;cursor:pointer;font-weight:400;margin:0}
.kw-tile:has(input:checked){border-color:var(--accent);background:var(--accent-weak)}
.kw-tile input{margin-top:3px;flex:none;width:auto}
.kw-tile b{display:block;font-size:14.5px}
.kw-tile small{display:block;color:var(--muted);font-size:13px;line-height:1.4;margin-top:3px}
.kw-panel{margin-top:16px}
.kw-drive{display:flex;gap:10px;align-items:flex-start;padding:10px;border:1px solid var(--border,#ddd);border-radius:8px;cursor:pointer;margin:0}
@media (max-width:600px){.kw-tiles{grid-template-columns:minmax(0,1fr)}}
</style>
{{/* ===== Лента шагов — когда путь выбран (флешка с ключом или новая флешка из архива) ===== */}}
{{if or $s.StagingID $s.Done}}
<div class="card"> <div class="card">
<ol style="list-style:none;padding:0;margin:0;display:grid;gap:12px"> <ol style="list-style:none;padding:0;margin:0;display:grid;gap:12px">
{{range $i, $step := $s.Steps}} {{range $i, $step := $s.Steps}}
@@ -27,6 +39,7 @@
</div> </div>
{{/* ===== Действие в зависимости от состояния ===== */}} {{/* ===== Действие в зависимости от состояния ===== */}}
{{end}}
{{if $s.Done}} {{if $s.Done}}
<div class="card" style="background:var(--ok-weak)"> <div class="card" style="background:var(--ok-weak)">
<h2>✓ Готово</h2> <h2>✓ Готово</h2>
@@ -68,10 +81,12 @@
{{else if $s.StagingID}} {{else if $s.StagingID}}
{{/* Архив загружен — выбор флешки + запись */}} {{/* Архив загружен — выбор флешки + запись */}}
<div class="card"> <div class="card">
<h2>Шаг 2 — выбор флешки и запись</h2> <h2>Шаг 2 — вставьте новую флешку и запишите ключ</h2>
<p class="muted">Архив распакован. Ключ: <code>{{fallbackTpl $s.VDK "—"}}</code>. <p class="muted">Архив распакован. Ключ: <code>{{fallbackTpl $s.VDK "—"}}</code>.
Выберите носитель — запись сделает бэкап, запишет ключ и справочник Вставьте в сервер чистую флешку — на неё запишется ключ и справочник сертификатов, затем bj
сертификатов, дотянет CRL и перезапустит ИШ.</p> подключит её к шлюзу и перезапустит ИШ. Копия прежнего содержимого флешки сохраняется на сервере.
Не используйте флешку с рабочим ключом другого сервера.</p>
{{if not .Drives}}<p style="margin:10px 0 0">Флешка не найдена — вставьте её в сервер и <a href="javascript:location.reload()">обновите страницу</a>.</p>{{else}}
<form method="post" action="/admin/setup/keywizard/install" style="margin-top:12px;display:grid;gap:12px;max-width:640px" <form method="post" action="/admin/setup/keywizard/install" style="margin-top:12px;display:grid;gap:12px;max-width:640px"
onsubmit="this.querySelector('button[type=submit]').disabled=true;this.querySelector('button[type=submit]').textContent='Устанавливаю… (до нескольких минут)';"> onsubmit="this.querySelector('button[type=submit]').disabled=true;this.querySelector('button[type=submit]').textContent='Устанавливаю… (до нескольких минут)';">
{{template "keyWizRet" .}} {{template "keyWizRet" .}}
@@ -90,8 +105,6 @@
</span> </span>
</label> </label>
{{end}} {{end}}
{{else}}
<p class="muted">Съёмные носители не обнаружены — будет использован текущий ключевой носитель ИШ по умолчанию.</p>
{{end}} {{end}}
</div> </div>
@@ -111,40 +124,54 @@
<span class="muted" style="font-size:12px">Пусто = имя берётся из архива автоматически.</span> <span class="muted" style="font-size:12px">Пусто = имя берётся из архива автоматически.</span>
</div> </div>
<button type="submit" class="btn btn-ok">Записать на флешку, сформировать справочник и проверить ИШ</button> <button type="submit" class="btn btn-ok">Записать ключ на флешку</button>
</form> </form>{{end}}
<form method="post" action="/admin/setup/keywizard/reset" style="margin-top:8px">{{template "keyWizRet" .}} <form method="post" action="/admin/setup/keywizard/reset" style="margin-top:8px">{{template "keyWizRet" .}}
<button type="submit" class="btn btn-secondary">Отмена / загрузить другой архив</button> <button type="submit" class="btn btn-secondary">Отмена</button>
</form> </form>
</div> </div>
{{else}} {{else}}
{{/* Начало — форма загрузки */}} {{/* Начало: где ключ — на подготовленной флешке или в архиве НРД (тогда — на новую флешку) */}}
<div class="card"> <div class="card">
<h2>Шаг 1 — откуда взять ключ</h2> <h2>Где ваш ключ?</h2>
<p class="muted"><b>Архив от НРД</b>: выберите .7z-архив с ключом и введите пароль архива.</p> <div class="kw-tiles">
<form method="post" action="/admin/setup/keywizard/upload" enctype="multipart/form-data" <label class="kw-tile"><input type="radio" name="kwmode" value="media" {{if .Drives}}checked{{end}}><span><b>Подключить флешку с ключом</b><small>Флешка уже подготовлена (например, с другого сервера bj): bj найдёт на ней ключ и подключит к шлюзу как есть — ничего на неё не записывая.</small></span></label>
style="margin-top:12px;display:grid;gap:10px;max-width:560px"> <label class="kw-tile"><input type="radio" name="kwmode" value="new" {{if not .Drives}}checked{{end}}><span><b>Сформировать ключ на новой флешке</b><small>Есть архив ключа от НРД и пароль: bj распакует его и запишет ключ на чистую флешку.</small></span></label>
{{template "keyWizRet" .}} </div>
<input type="file" name="archive" accept=".7z,.zip" required>
<input type="password" name="password" placeholder="Пароль архива (например 11)" autocomplete="off"> <div class="kw-panel" id="kw-media">
<button type="submit" class="btn btn-ok">Загрузить и распаковать</button>
</form>
<div style="margin-top:18px;padding-top:14px;border-top:1px solid var(--border)">
<p class="muted" style="margin:0 0 8px"><b>У меня уже есть подготовленная флешка</b> — использовать ключ с неё (например, флешка с другого сервера bj). Флешка используется как есть: bj её только читает, ничего на неё не записывает. Архив не нужен.</p>
{{if .Drives}} {{if .Drives}}
<form method="post" action="/admin/setup/keywizard/from-media" style="display:grid;gap:8px;max-width:640px"> <form method="post" action="/admin/setup/keywizard/from-media" style="display:grid;gap:8px;max-width:640px">
{{template "keyWizRet" .}} {{template "keyWizRet" .}}
<p class="muted" style="margin:0">Флешка с ключом:</p>
{{range $i, $d := .Drives}} {{range $i, $d := .Drives}}
<label style="display:flex;gap:10px;align-items:flex-start;padding:10px;border:1px solid var(--border,#ddd);border-radius:8px;cursor:pointer"> <label class="kw-drive">
<input type="radio" name="source_device" value="{{$d.Device}}" {{if eq $i 0}}checked{{end}} style="margin-top:3px"> <input type="radio" name="source_device" value="{{$d.Device}}" {{if eq $i 0}}checked{{end}} style="margin-top:3px;width:auto">
<span><b>{{fallbackTpl $d.Model "USB-носитель"}}</b> · {{$d.Size}} · {{$d.FSType}}{{if $d.Label}} · метка «{{$d.Label}}»{{end}}<br> <span><b>{{fallbackTpl $d.Model "USB-носитель"}}</b> · {{$d.Size}} · {{$d.FSType}}{{if $d.Label}} · метка «{{$d.Label}}»{{end}}<br>
<span class="muted" style="font-size:12px">{{$d.Device}}{{if $d.IsKeymedia}} · <b style="color:var(--accent)">сейчас ключевой носитель ИШ</b>{{end}}</span></span> <span class="muted" style="font-size:12px">{{$d.Device}}{{if $d.IsKeymedia}} · <b style="color:var(--accent)">сейчас ключевой носитель ИШ</b>{{end}}</span></span>
</label> </label>
{{end}} {{end}}
<button type="submit" class="btn btn-secondary" onclick="this.textContent='Читаю флешку…'">Использовать ключ с флешки</button> <button type="submit" class="btn btn-ok" onclick="this.textContent='Ищу ключ на флешке…'">Подключить флешку</button>
</form>
{{else}}<p style="margin:0">Флешка не найдена — вставьте флешку с ключом в сервер и <a href="javascript:location.reload()">обновите страницу</a>.</p>{{end}}
</div>
<div class="kw-panel" id="kw-new" hidden>
<p class="muted" style="margin:0 0 8px">Шаг 1 — архив ключа от НРД (.7z) и пароль архива. Флешку bj попросит после распаковки.</p>
<form method="post" action="/admin/setup/keywizard/upload" enctype="multipart/form-data"
style="display:grid;gap:10px;max-width:560px">
{{template "keyWizRet" .}}
<input type="file" name="archive" accept=".7z,.zip" required>
<input type="password" name="password" placeholder="Пароль архива (например 11)" autocomplete="off">
<button type="submit" class="btn btn-ok">Загрузить и распаковать</button>
</form> </form>
{{else}}<p class="muted" style="font-size:13px;margin:0">Флешка не найдена — вставьте её в сервер и обновите страницу.</p>{{end}}
</div> </div>
</div> </div>
<script>(function(){
var r=document.querySelectorAll('input[name=kwmode]');
function sync(){ var m=document.querySelector('input[name=kwmode]:checked'); m=m?m.value:'media';
document.getElementById('kw-media').hidden=m!=='media'; document.getElementById('kw-new').hidden=m!=='new'; }
r.forEach(function(x){ x.addEventListener('change', sync); }); sync();
})();</script>
{{end}} {{end}}
{{end}} {{end}}