Установка: приветственный экран консоли (bj-welcome: адрес веб-входа и состояние, всегда), ядро с образа без ключа продукта (--bundle), ключ продукта на первой веб-странице, повтор вопроса при каждой загрузке
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q4YVo4UMopqT5DzicqpPjL
This commit is contained in:
@@ -69,6 +69,7 @@ type installer struct {
|
|||||||
token string
|
token string
|
||||||
lic *license.License
|
lic *license.License
|
||||||
setupCode string // одноразовый код первичной настройки (итоговый экран)
|
setupCode string // одноразовый код первичной настройки (итоговый экран)
|
||||||
|
bundle string // локальный релиз (образ ISO): ядро без ключа продукта
|
||||||
shown bool
|
shown bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,6 +100,7 @@ func cmdInstall(args []string) {
|
|||||||
fl := flag.NewFlagSet("install", flag.ExitOnError)
|
fl := flag.NewFlagSet("install", flag.ExitOnError)
|
||||||
in := &installer{out: newConsole()}
|
in := &installer{out: newConsole()}
|
||||||
fl.StringVar(&in.code, "code", "", "ключ продукта (код активации)")
|
fl.StringVar(&in.code, "code", "", "ключ продукта (код активации)")
|
||||||
|
fl.StringVar(&in.bundle, "bundle", "", "каталог с релизом (manifest.json и файлы) — установка ядра без ключа продукта, ключ вводится в браузере")
|
||||||
fl.StringVar(&in.server, "server", "https://lic.zetit.ru", "сервер лицензий и обновлений")
|
fl.StringVar(&in.server, "server", "https://lic.zetit.ru", "сервер лицензий и обновлений")
|
||||||
fl.StringVar(&in.channel, "channel", "stable", "канал обновлений")
|
fl.StringVar(&in.channel, "channel", "stable", "канал обновлений")
|
||||||
fl.BoolVar(&in.dryRun, "dry-run", false, "только проверить систему")
|
fl.BoolVar(&in.dryRun, "dry-run", false, "только проверить систему")
|
||||||
@@ -136,7 +138,7 @@ func (in *installer) run() error {
|
|||||||
if in.root == "" && !in.dryRun && os.Geteuid() != 0 {
|
if in.root == "" && !in.dryRun && os.Geteuid() != 0 {
|
||||||
return errors.New("установщик запускается от root: sudo bj-installer install …")
|
return errors.New("установщик запускается от root: sudo bj-installer install …")
|
||||||
}
|
}
|
||||||
if !in.dryRun && in.steps == nil && strings.TrimSpace(in.code) == "" {
|
if !in.dryRun && in.steps == nil && strings.TrimSpace(in.code) == "" && in.bundle == "" {
|
||||||
return errors.New("не указан ключ продукта: bj-installer install --code BJ-XXXX-XXXX-XXXX (он есть в портале клиента https://lic.zetit.ru/portal/)")
|
return errors.New("не указан ключ продукта: bj-installer install --code BJ-XXXX-XXXX-XXXX (он есть в портале клиента https://lic.zetit.ru/portal/)")
|
||||||
}
|
}
|
||||||
all := []step{
|
all := []step{
|
||||||
@@ -356,6 +358,9 @@ func machineHash() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (in *installer) stepLicense() (string, error) {
|
func (in *installer) stepLicense() (string, error) {
|
||||||
|
if strings.TrimSpace(in.code) == "" && in.bundle != "" {
|
||||||
|
return "пропущена: ключ продукта вводится в браузере на первой странице настройки", nil
|
||||||
|
}
|
||||||
setup := in.loadSetup()
|
setup := in.loadSetup()
|
||||||
lic := section(setup, "license")
|
lic := section(setup, "license")
|
||||||
installID, _ := lic["install_id"].(string)
|
installID, _ := lic["install_id"].(string)
|
||||||
@@ -516,6 +521,9 @@ func unitTemplate(unit string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (in *installer) stepComponents() (string, error) {
|
func (in *installer) stepComponents() (string, error) {
|
||||||
|
if in.bundle != "" {
|
||||||
|
return in.componentsFromBundle()
|
||||||
|
}
|
||||||
if err := in.ensureToken(); err != nil {
|
if err := in.ensureToken(); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -584,6 +592,77 @@ func (in *installer) stepComponents() (string, error) {
|
|||||||
return detail, nil
|
return detail, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// componentsFromBundle — установка из локального релиза (образ ISO): подписанный manifest.json и файлы
|
||||||
|
// рядом. Без лицензии ставится только ядро; модули — после активации ключа, из раздела «Обновления».
|
||||||
|
func (in *installer) componentsFromBundle() (string, error) {
|
||||||
|
pub, err := release.ParsePublicKey(release.DefaultPublicKey)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
_ = in.ensureToken() // лицензии может не быть — тогда только ядро
|
||||||
|
sm, m, err := release.ReadStagedManifest(in.bundle, pub)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("релиз в %s: %v", in.bundle, err)
|
||||||
|
}
|
||||||
|
staging := filepath.Join(in.P(release.StagingBase), fmt.Sprintf("install-%d", time.Now().UnixNano()))
|
||||||
|
if err := os.MkdirAll(staging, 0o750); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(staging)
|
||||||
|
var names, skipped []string
|
||||||
|
for _, c := range release.Components {
|
||||||
|
var art *release.Artifact
|
||||||
|
for i := range m.Artifacts {
|
||||||
|
if m.Artifacts[i].Name == c.Name {
|
||||||
|
art = &m.Artifacts[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case art == nil:
|
||||||
|
case !c.Core && (in.lic == nil || !in.licensed(c.Feature)):
|
||||||
|
skipped = append(skipped, c.Title)
|
||||||
|
case !unitTemplate(c.Unit):
|
||||||
|
skipped = append(skipped, c.Title)
|
||||||
|
default:
|
||||||
|
src := filepath.Join(in.bundle, art.File)
|
||||||
|
if _, err := os.Stat(src); err != nil {
|
||||||
|
return "", fmt.Errorf("в релизе нет файла %s", art.File)
|
||||||
|
}
|
||||||
|
body, err := os.ReadFile(src)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(staging, art.File), body, 0o640); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
names = append(names, c.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(names) == 0 {
|
||||||
|
return "", fmt.Errorf("в релизе %s нет компонентов ядра", m.Version)
|
||||||
|
}
|
||||||
|
b, _ := json.Marshal(sm)
|
||||||
|
_ = os.WriteFile(filepath.Join(staging, "manifest.json"), b, 0o640)
|
||||||
|
rb, _ := json.Marshal(map[string][]string{"names": names})
|
||||||
|
_ = os.WriteFile(filepath.Join(staging, "request.json"), rb, 0o640)
|
||||||
|
res := release.InstallStaged(staging, release.InstallOptions{Root: in.root, PubKey: pub, AllowNew: true})
|
||||||
|
if !res.OK {
|
||||||
|
return "", errors.New(res.Error)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(in.P("/etc/bj/update.pub"), []byte(release.DefaultPublicKey+"\n"), 0o644); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
var list []string
|
||||||
|
for _, x := range res.Installed {
|
||||||
|
list = append(list, x["name"]+" "+x["version"])
|
||||||
|
}
|
||||||
|
detail := "релиз " + m.Version + " с образа: " + strings.Join(list, ", ")
|
||||||
|
if len(skipped) > 0 {
|
||||||
|
detail += "; модули " + strings.Join(skipped, ", ") + " установятся после активации ключа продукта"
|
||||||
|
}
|
||||||
|
return detail, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (in *installer) stepServices() (string, error) {
|
func (in *installer) stepServices() (string, error) {
|
||||||
var files []string
|
var files []string
|
||||||
err := fs.WalkDir(deploy.Files, ".", func(p string, d fs.DirEntry, err error) error {
|
err := fs.WalkDir(deploy.Files, ".", func(p string, d fs.DirEntry, err error) error {
|
||||||
@@ -609,7 +688,7 @@ func (in *installer) stepServices() (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
enable := []string{"enable", "bj-keymedia.service", "bj-server.service"}
|
enable := []string{"enable", "bj-keymedia.service", "bj-server.service", "bj-welcome.service", "bj-welcome.timer"}
|
||||||
// службы установленных модулей (bj-npr и т. п.), для которых есть unit-файл
|
// службы установленных модулей (bj-npr и т. п.), для которых есть unit-файл
|
||||||
for name := range release.LoadInstalled(in.P(release.InstalledFile)) {
|
for name := range release.LoadInstalled(in.P(release.InstalledFile)) {
|
||||||
if c, ok := release.ComponentByName(name); ok && !c.Core && c.Unit != "" && unitTemplate(c.Unit) {
|
if c, ok := release.ComponentByName(name); ok && !c.Core && c.Unit != "" && unitTemplate(c.Unit) {
|
||||||
@@ -678,12 +757,20 @@ func (in *installer) newFirstRunCode() (string, error) {
|
|||||||
}
|
}
|
||||||
in.chownBJ(in.P(firstRunFile))
|
in.chownBJ(in.P(firstRunFile))
|
||||||
code := digits[:4] + "-" + digits[4:]
|
code := digits[:4] + "-" + digits[4:]
|
||||||
url := setupURLs()
|
in.refreshWelcome()
|
||||||
issue := fmt.Sprintf("Bridge&Join, первичная настройка: откройте в браузере %s/first-run и введите код %s\n\n", firstOr(url, "http://<адрес сервера>:8080"), code)
|
|
||||||
_ = writeFile(in.P("/etc/issue.d/bj-setup.issue"), []byte(issue), 0o644)
|
|
||||||
return code, nil
|
return code, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// refreshWelcome — обновить приветственный экран консоли (адреса и состояние bj).
|
||||||
|
func (in *installer) refreshWelcome() {
|
||||||
|
if in.root != "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := os.Stat("/usr/local/sbin/bj-welcome"); err == nil {
|
||||||
|
_, _ = run("/usr/local/sbin/bj-welcome")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func cmdFirstRunCode(args []string) {
|
func cmdFirstRunCode(args []string) {
|
||||||
in := &installer{out: newConsole()}
|
in := &installer{out: newConsole()}
|
||||||
if os.Geteuid() != 0 {
|
if os.Geteuid() != 0 {
|
||||||
@@ -860,6 +947,9 @@ func (c *console) done(code string, l *license.License) {
|
|||||||
} else {
|
} else {
|
||||||
fmt.Fprintf(c.w, " Адрес bj: %s\n", c.paint("1", firstOr(urls, fmt.Sprintf("http://<адрес сервера>:%d", bjPort))))
|
fmt.Fprintf(c.w, " Адрес bj: %s\n", c.paint("1", firstOr(urls, fmt.Sprintf("http://<адрес сервера>:%d", bjPort))))
|
||||||
}
|
}
|
||||||
|
if l == nil {
|
||||||
|
fmt.Fprintf(c.w, "\n Ключ продукта ещё не введён — его спросит первая страница в браузере\n (или позже: «Настройка → Лицензия»). Он есть в портале клиента.\n")
|
||||||
|
}
|
||||||
if l != nil {
|
if l != nil {
|
||||||
fmt.Fprintf(c.w, "\n Лицензия: %s, план %s, ключ до %s, модули: %s\n", l.Tenant, l.Plan, l.ExpiresAt.Local().Format("02.01.2006"), featuresText(l))
|
fmt.Fprintf(c.w, "\n Лицензия: %s, план %s, ключ до %s, модули: %s\n", l.Tenant, l.Plan, l.ExpiresAt.Local().Format("02.01.2006"), featuresText(l))
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -6,5 +6,5 @@ import "embed"
|
|||||||
|
|
||||||
// Files — linux/sbin/* (в /usr/local/sbin) и systemd/*.service (в /etc/systemd/system).
|
// Files — linux/sbin/* (в /usr/local/sbin) и systemd/*.service (в /etc/systemd/system).
|
||||||
//
|
//
|
||||||
//go:embed linux/sbin/* systemd/bj-server.service systemd/bj-crypto.service systemd/bj-keymedia.service systemd/bj-npr.service systemd/bj-quikexport.service systemd/bj-lk.service
|
//go:embed linux/sbin/* systemd/bj-server.service systemd/bj-crypto.service systemd/bj-keymedia.service systemd/bj-npr.service systemd/bj-quikexport.service systemd/bj-lk.service systemd/bj-welcome.service systemd/bj-welcome.timer
|
||||||
var Files embed.FS
|
var Files embed.FS
|
||||||
|
|||||||
+36
-17
@@ -1,41 +1,60 @@
|
|||||||
#!/bin/bash
|
#!/bin/sh
|
||||||
# Первая загрузка сервера, установленного с образа Bridge&Join: на экране сервера спрашивает ключ
|
# Первая загрузка сервера, установленного с образа Bridge&Join. Спрашивает ключ продукта на консоли;
|
||||||
# продукта и запускает bj-installer. После успешной установки служба выключается; адрес первичной
|
# без ключа ставит ядро bj с образа (/var/lib/bj/bundle), а ключ вводится в браузере на первой
|
||||||
# настройки и одноразовый код остаются на экране входа (/etc/issue.d/bj-setup.issue).
|
# странице настройки. Пока bj не установлен, вопрос повторяется при каждой загрузке (с таймаутом),
|
||||||
|
# адрес и подсказки всегда есть на экране входа (bj-welcome).
|
||||||
set -u
|
set -u
|
||||||
MARK=/var/lib/bj/.bj/installed-from-iso
|
MARK=/var/lib/bj/.bj/installed-from-iso
|
||||||
SERVER=https://lic.zetit.ru
|
SERVER=https://lic.zetit.ru
|
||||||
CHANNEL=stable
|
CHANNEL=stable
|
||||||
|
BUNDLE=/var/lib/bj/bundle
|
||||||
[ -s /etc/bj/channel ] && CHANNEL=$(tr -d ' \n' < /etc/bj/channel)
|
[ -s /etc/bj/channel ] && CHANNEL=$(tr -d ' \n' < /etc/bj/channel)
|
||||||
|
|
||||||
finish() {
|
finish() {
|
||||||
mkdir -p "$(dirname "$MARK")" && date -Iseconds > "$MARK"
|
mkdir -p "$(dirname "$MARK")" && date -Iseconds > "$MARK"
|
||||||
systemctl disable bj-firstboot.service >/dev/null 2>&1
|
systemctl disable bj-firstboot.service >/dev/null 2>&1
|
||||||
|
[ -x /usr/local/sbin/bj-welcome ] && /usr/local/sbin/bj-welcome
|
||||||
}
|
}
|
||||||
|
urls() { ip -4 -o addr show scope global 2>/dev/null | awk '{split($4,a,"/"); printf " http://%s:8080%s\n", a[1], "'"$1"'"}'; }
|
||||||
clear
|
clear
|
||||||
printf '\n Bridge&Join: установка на сервер\n ────────────────────────────────────────────────\n\n'
|
printf '\n Bridge&Join: установка на сервер\n ────────────────────────────────────────────────────────────\n'
|
||||||
printf ' Debian установлен. Осталось установить bj.\n'
|
printf ' Debian установлен. Осталось установить bj.\n\n'
|
||||||
printf ' Нужен ключ продукта (BJ-XXXX-XXXX-XXXX): он есть в портале клиента %s/portal/\n\n' "$SERVER"
|
if [ -f "$BUNDLE/manifest.json" ]; then
|
||||||
|
printf ' Ключ продукта (BJ-XXXX-XXXX-XXXX) есть в портале клиента %s/portal/.\n' "$SERVER"
|
||||||
for i in $(seq 1 60); do
|
printf ' Вводить его здесь необязательно: нажмите Enter — ядро bj установится с образа,\n а ключ вы введёте в браузере на первой странице настройки (там его удобно вставить).\n\n'
|
||||||
|
else
|
||||||
|
printf ' Нужен ключ продукта (BJ-XXXX-XXXX-XXXX): он есть в портале клиента %s/portal/\n\n' "$SERVER"
|
||||||
|
fi
|
||||||
|
for i in $(seq 1 30); do
|
||||||
curl -fsS -m 5 -o /dev/null "$SERVER/healthz" && break
|
curl -fsS -m 5 -o /dev/null "$SERVER/healthz" && break
|
||||||
[ "$i" = 1 ] && printf ' Жду сеть и связь с %s…\n' "$SERVER"
|
[ "$i" = 1 ] && printf ' Жду сеть и связь с %s…\n' "$SERVER"
|
||||||
sleep 2
|
sleep 2
|
||||||
done
|
done
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
read -r -p " Ключ продукта (пусто — установить позже): " CODE
|
CODE=""
|
||||||
CODE=$(printf '%s' "$CODE" | tr -d ' ' | tr '[:lower:]' '[:upper:]')
|
if [ -f "$BUNDLE/manifest.json" ]; then
|
||||||
|
read -t 120 -r -p " Ключ продукта (Enter — установить с образа, ключ ввести в браузере): " CODE || true
|
||||||
|
else
|
||||||
|
read -t 120 -r -p " Ключ продукта (Enter — установить позже): " CODE || true
|
||||||
|
fi
|
||||||
|
CODE=$(printf '%s' "${CODE:-}" | tr -d ' ' | tr '[:lower:]' '[:upper:]')
|
||||||
if [ -z "$CODE" ]; then
|
if [ -z "$CODE" ]; then
|
||||||
printf '\n Установка bj отложена. Позже выполните: sudo bj-installer install --code КЛЮЧ-ПРОДУКТА\n\n'
|
if [ -f "$BUNDLE/manifest.json" ]; then
|
||||||
finish
|
if /usr/local/sbin/bj-installer install --bundle "$BUNDLE" --server "$SERVER" --channel "$CHANNEL"; then
|
||||||
|
finish
|
||||||
|
printf '\n Ключ продукта спросит первая страница настройки в браузере:\n'; urls /first-run; printf '\n'
|
||||||
|
read -t 60 -r -p " Нажмите Enter, чтобы перейти к экрану входа. " _ || true
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
printf '\n Установка с образа не завершилась. Подробности выше; повторить:\n sudo bj-installer install --bundle %s\n\n' "$BUNDLE"
|
||||||
|
sleep 5; exit 0
|
||||||
|
fi
|
||||||
|
printf '\n Установка bj отложена. Вопрос повторится при следующей загрузке; вручную:\n sudo bj-installer install --code КЛЮЧ-ПРОДУКТА\n\n'
|
||||||
sleep 3
|
sleep 3
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
if /usr/local/sbin/bj-installer install --code "$CODE" --server "$SERVER" --channel "$CHANNEL"; then
|
if /usr/local/sbin/bj-installer install --code "$CODE" --server "$SERVER" --channel "$CHANNEL"; then
|
||||||
finish
|
finish
|
||||||
read -r -p " Нажмите Enter, чтобы перейти к экрану входа. " _
|
read -t 60 -r -p " Нажмите Enter, чтобы перейти к экрану входа. " _ || true
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
printf '\n Установка не завершилась. Проверьте ключ продукта и сеть и попробуйте ещё раз.\n\n'
|
printf '\n Установка не завершилась. Проверьте ключ продукта и сеть и попробуйте ещё раз.\n\n'
|
||||||
|
|||||||
+15
-1
@@ -1,7 +1,8 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# Сборка образа ISO Bridge&Join на основе официального Debian 13 netinst.
|
# Сборка образа ISO Bridge&Join на основе официального Debian 13 netinst.
|
||||||
#
|
#
|
||||||
# deploy/iso/build-iso.sh <debian-13.x-amd64-netinst.iso> <bj-installer> <версия> [канал] [выходной.iso]
|
# deploy/iso/build-iso.sh <debian-13.x-amd64-netinst.iso> <bj-installer> <версия> [канал] [выходной.iso] [каталог релиза]
|
||||||
|
# Каталог релиза (dist с manifest.json) — ядро bj кладётся в образ: установка без ключа продукта, ключ вводится в браузере.
|
||||||
#
|
#
|
||||||
# В образ добавляются: файл ответов установщику (preseed.cfg), bj-installer и служба первой загрузки.
|
# В образ добавляются: файл ответов установщику (preseed.cfg), bj-installer и служба первой загрузки.
|
||||||
# Загрузочное меню BIOS (isolinux) и UEFI (grub) по умолчанию запускает установку с этими ответами.
|
# Загрузочное меню BIOS (isolinux) и UEFI (grub) по умолчанию запускает установку с этими ответами.
|
||||||
@@ -13,6 +14,7 @@ INSTALLER=${2:?файл bj-installer}
|
|||||||
VERSION=${3:?версия образа, например 2026.10.1}
|
VERSION=${3:?версия образа, например 2026.10.1}
|
||||||
CHANNEL=${4:-stable}
|
CHANNEL=${4:-stable}
|
||||||
OUT=${5:-bj-debian13-$VERSION.iso}
|
OUT=${5:-bj-debian13-$VERSION.iso}
|
||||||
|
BUNDLE=${6:-}
|
||||||
HERE=$(cd "$(dirname "$0")" && pwd)
|
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||||
command -v xorriso >/dev/null || { echo "нужен xorriso: apt-get install xorriso"; exit 1; }
|
command -v xorriso >/dev/null || { echo "нужен xorriso: apt-get install xorriso"; exit 1; }
|
||||||
WORK=$(mktemp -d)
|
WORK=$(mktemp -d)
|
||||||
@@ -25,6 +27,18 @@ install -m 0755 "$HERE/bj-firstboot" "$WORK/bj/bj-firstboot"
|
|||||||
cp "$HERE/bj-firstboot.service" "$WORK/bj/bj-firstboot.service"
|
cp "$HERE/bj-firstboot.service" "$WORK/bj/bj-firstboot.service"
|
||||||
echo "$CHANNEL" > "$WORK/bj/channel"
|
echo "$CHANNEL" > "$WORK/bj/channel"
|
||||||
echo "$VERSION" > "$WORK/bj/version"
|
echo "$VERSION" > "$WORK/bj/version"
|
||||||
|
# приветственный экран консоли (адрес веб-входа) — ставится ещё до установки bj
|
||||||
|
install -m 0755 "$HERE/../linux/sbin/bj-welcome" "$WORK/bj/bj-welcome"
|
||||||
|
cp "$HERE/../systemd/bj-welcome.service" "$HERE/../systemd/bj-welcome.timer" "$WORK/bj/"
|
||||||
|
# ядро bj с образа: подписанный манифест и файлы компонентов ядра (bj-server, bj-component-install)
|
||||||
|
if [ -n "$BUNDLE" ]; then
|
||||||
|
[ -s "$BUNDLE/manifest.json" ] || { echo "в $BUNDLE нет manifest.json"; exit 1; }
|
||||||
|
mkdir -p "$WORK/bj/bundle"; cp "$BUNDLE/manifest.json" "$WORK/bj/bundle/"
|
||||||
|
for f in $(python3 -c 'import json,sys;m=json.load(open(sys.argv[1]));m=m.get("manifest",m);print(" ".join(a["file"] for a in m["artifacts"] if a["name"] in ("bj-server","bj-component-install")))' "$BUNDLE/manifest.json"); do
|
||||||
|
cp "$BUNDLE/$f" "$WORK/bj/bundle/"
|
||||||
|
done
|
||||||
|
echo "в образ добавлено ядро bj: $(ls "$WORK/bj/bundle" | tr '\n' ' ')"
|
||||||
|
fi
|
||||||
|
|
||||||
BOOT="auto=true priority=high preseed/file=/cdrom/preseed/bj.cfg language=ru country=RU locale=ru_RU.UTF-8 keyboard-configuration/xkb-keymap=ru"
|
BOOT="auto=true priority=high preseed/file=/cdrom/preseed/bj.cfg language=ru country=RU locale=ru_RU.UTF-8 keyboard-configuration/xkb-keymap=ru"
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
#_preseed_V1
|
#_preseed_V1
|
||||||
# Ответы установщику Debian 13 для образа Bridge&Join (deploy/iso/build-iso.sh).
|
# Ответы установщику Debian 13 для образа Bridge&Join (deploy/iso/build-iso.sh).
|
||||||
# Спрашиваются только пароли, диск (если их несколько) и подтверждение стирания; остальное задано здесь.
|
# Спрашиваются только пароли, диск (если их несколько) и подтверждение стирания; остальное задано здесь.
|
||||||
# После установки служба bj-firstboot на экране сервера спросит ключ продукта и установит bj.
|
# После установки служба bj-firstboot на экране сервера спросит ключ продукта (или установит ядро с образа —
|
||||||
|
# ключ тогда вводится в браузере); bj-welcome показывает адрес веб-входа на экране входа в консоль.
|
||||||
|
|
||||||
# Язык, клавиатура, время
|
# Язык, клавиатура, время
|
||||||
d-i debian-installer/language string ru
|
d-i debian-installer/language string ru
|
||||||
@@ -69,6 +70,8 @@ d-i preseed/late_command string \
|
|||||||
install -m 0644 /cdrom/bj/bj-firstboot.service /target/etc/systemd/system/bj-firstboot.service; \
|
install -m 0644 /cdrom/bj/bj-firstboot.service /target/etc/systemd/system/bj-firstboot.service; \
|
||||||
mkdir -p /target/etc/bj; \
|
mkdir -p /target/etc/bj; \
|
||||||
if [ -f /cdrom/bj/channel ]; then install -m 0644 /cdrom/bj/channel /target/etc/bj/channel; fi; \
|
if [ -f /cdrom/bj/channel ]; then install -m 0644 /cdrom/bj/channel /target/etc/bj/channel; fi; \
|
||||||
|
if [ -d /cdrom/bj/bundle ]; then mkdir -p /target/var/lib/bj/bundle && cp -r /cdrom/bj/bundle/. /target/var/lib/bj/bundle/; fi; \
|
||||||
|
if [ -f /cdrom/bj/bj-welcome ]; then install -m 0755 /cdrom/bj/bj-welcome /target/usr/local/sbin/bj-welcome; install -m 0644 /cdrom/bj/bj-welcome.service /target/etc/systemd/system/; install -m 0644 /cdrom/bj/bj-welcome.timer /target/etc/systemd/system/; in-target systemctl enable bj-welcome.service bj-welcome.timer; fi; \
|
||||||
in-target systemctl enable bj-firstboot.service
|
in-target systemctl enable bj-firstboot.service
|
||||||
|
|
||||||
d-i finish-install/reboot_in_progress note
|
d-i finish-install/reboot_in_progress note
|
||||||
|
|||||||
Executable
+66
@@ -0,0 +1,66 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# bj-welcome — приветственный экран на консоли до входа (/etc/issue.d/10-bj.issue):
|
||||||
|
# адреса веб-входа и состояние bj. Вызывается службой bj-welcome при загрузке и по таймеру,
|
||||||
|
# а также установщиком после шагов, меняющих состояние. Читает только локальные файлы.
|
||||||
|
set -u
|
||||||
|
ISSUE=/etc/issue.d/10-bj.issue
|
||||||
|
BJ=/opt/bj/bj-server
|
||||||
|
SETUP=/var/lib/bj/.bj/setup.json
|
||||||
|
USERS=/var/lib/bj/.bj/users.json
|
||||||
|
FIRSTRUN=/var/lib/bj/.bj/first-run.json
|
||||||
|
PORT=8080
|
||||||
|
HOST=$(hostname 2>/dev/null || echo bj)
|
||||||
|
ADDRS=$(ip -4 -o addr show scope global 2>/dev/null | awk '{split($4,a,"/"); print a[1]}' | tr '\n' ' ')
|
||||||
|
[ -z "$ADDRS" ] && ADDRS="<адрес не получен: проверьте сеть>"
|
||||||
|
url() { for a in $ADDRS; do case "$a" in \<*) printf ' http://%s:%s%s\n' "<IP сервера>" "$PORT" "$1";; *) printf ' http://%s:%s%s\n' "$a" "$PORT" "$1";; esac; done; }
|
||||||
|
lic=""
|
||||||
|
if [ -r "$SETUP" ]; then
|
||||||
|
lic=$(python3 - "$SETUP" 2>/dev/null <<'PY'
|
||||||
|
import json,sys
|
||||||
|
try:
|
||||||
|
d=json.load(open(sys.argv[1])); l=d.get("license",{})
|
||||||
|
print("да" if l.get("key") else "нет", l.get("last_msg","")[:60])
|
||||||
|
except Exception: print("")
|
||||||
|
PY
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
{
|
||||||
|
printf '\n ┌────────────────────────────────────────────────────────────────┐\n'
|
||||||
|
printf ' │ Bridge&Join (bj) — шлюз переводов ценных бумаг M2M / НРД │\n'
|
||||||
|
printf ' └────────────────────────────────────────────────────────────────┘\n'
|
||||||
|
printf ' Сервер: %s Адреса: %s\n\n' "$HOST" "$ADDRS"
|
||||||
|
if [ ! -x "$BJ" ]; then
|
||||||
|
printf ' Состояние: bj НЕ УСТАНОВЛЕН.\n'
|
||||||
|
if [ -f /var/lib/bj/bundle/manifest.json ]; then
|
||||||
|
printf ' Установка с образа (ключ продукта — потом, в браузере):\n sudo bj-installer install --bundle /var/lib/bj/bundle\n'
|
||||||
|
printf ' Или с ключом продукта: sudo bj-installer install --code BJ-XXXX-XXXX-XXXX\n'
|
||||||
|
else
|
||||||
|
printf ' Установка: sudo bj-installer install --code BJ-XXXX-XXXX-XXXX (ключ — в портале клиента)\n'
|
||||||
|
fi
|
||||||
|
elif ! grep -q '"login"' "$USERS" 2>/dev/null; then
|
||||||
|
printf ' Состояние: bj установлен, ПЕРВИЧНАЯ НАСТРОЙКА НЕ ВЫПОЛНЕНА.\n'
|
||||||
|
printf ' Откройте в браузере на своём компьютере:\n'; url /first-run
|
||||||
|
if [ -f "$FIRSTRUN" ]; then
|
||||||
|
printf ' Введите одноразовый код, показанный установщиком. Код потерян или истёк —\n новый код: sudo bj-installer first-run-code\n'
|
||||||
|
else
|
||||||
|
printf ' Нужен одноразовый код: sudo bj-installer first-run-code\n'
|
||||||
|
fi
|
||||||
|
case "$lic" in да*) ;; *) printf ' Ключ продукта ещё не введён — его спросит первая страница.\n';; esac
|
||||||
|
else
|
||||||
|
printf ' Состояние: bj работает. Вход в веб-интерфейс:\n'; url /admin
|
||||||
|
case "$lic" in
|
||||||
|
да*) printf ' Лицензия: %s\n' "${lic#да }";;
|
||||||
|
*) printf ' Лицензия НЕ АКТИВИРОВАНА: введите ключ продукта в «Настройка → Лицензия».\n';;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
printf '\n Вход в консоль сервера — учётной записью ОС (для обслуживания). Веб-вход — по адресу выше.\n\n'
|
||||||
|
} > "$ISSUE.tmp"
|
||||||
|
if ! cmp -s "$ISSUE.tmp" "$ISSUE" 2>/dev/null; then
|
||||||
|
mv -f "$ISSUE.tmp" "$ISSUE"
|
||||||
|
# обновить приглашение на tty1, если там никто не вошёл
|
||||||
|
if command -v systemctl >/dev/null && ! who 2>/dev/null | grep -qE '^\S+\s+tty1\b'; then
|
||||||
|
systemctl try-restart getty@tty1.service 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
rm -f "$ISSUE.tmp"
|
||||||
|
fi
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Bridge&Join: приветственный экран на консоли (адрес веб-входа и состояние)
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
Before=getty@tty1.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/local/sbin/bj-welcome
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Bridge&Join: обновление приветственного экрана (адреса, состояние)
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnBootSec=1min
|
||||||
|
OnUnitActiveSec=2min
|
||||||
|
AccuracySec=30s
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
@@ -7,10 +7,12 @@ package lkgateway
|
|||||||
// (открытый режим до создания первого пользователя).
|
// (открытый режим до создания первого пользователя).
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"html/template"
|
"html/template"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -36,6 +38,8 @@ func firstRunPending() bool {
|
|||||||
|
|
||||||
type firstRunData struct {
|
type firstRunData struct {
|
||||||
Error, Login, Name string
|
Error, Login, Name string
|
||||||
|
NeedKey bool // лицензия ещё не активирована — предложить ввести ключ продукта здесь
|
||||||
|
ProductKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
var firstRunTpl = template.Must(template.New("first-run").Parse(firstRunHTML))
|
var firstRunTpl = template.Must(template.New("first-run").Parse(firstRunHTML))
|
||||||
@@ -47,11 +51,13 @@ func registerFirstRun(mux *http.ServeMux) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
var d firstRunData
|
var d firstRunData
|
||||||
|
d.NeedKey = globalRC != nil && !licenseStatus(globalRC).Valid
|
||||||
w.Header().Set("Cache-Control", "no-store")
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
if r.Method == http.MethodPost {
|
if r.Method == http.MethodPost {
|
||||||
ip := clientIP(r)
|
ip := clientIP(r)
|
||||||
key := "first-run|" + ip
|
key := "first-run|" + ip
|
||||||
d.Login, d.Name = strings.TrimSpace(r.FormValue("login")), strings.TrimSpace(r.FormValue("name"))
|
d.Login, d.Name = strings.TrimSpace(r.FormValue("login")), strings.TrimSpace(r.FormValue("name"))
|
||||||
|
d.ProductKey = strings.ToUpper(strings.NewReplacer(" ", "").Replace(strings.TrimSpace(r.FormValue("product_key"))))
|
||||||
pw, pw2 := r.FormValue("password"), r.FormValue("password2")
|
pw, pw2 := r.FormValue("password"), r.FormValue("password2")
|
||||||
code := strings.NewReplacer("-", "", " ", "").Replace(strings.TrimSpace(r.FormValue("code")))
|
code := strings.NewReplacer("-", "", " ", "").Replace(strings.TrimSpace(r.FormValue("code")))
|
||||||
var st firstRunState
|
var st firstRunState
|
||||||
@@ -88,6 +94,23 @@ func registerFirstRun(mux *http.ServeMux) {
|
|||||||
s := globalSessions.New(u.Login, ip, r.UserAgent())
|
s := globalSessions.New(u.Login, ip, r.UserAgent())
|
||||||
http.SetCookie(w, &http.Cookie{Name: sessionCookie, Value: s.Token, Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode, Secure: r.TLS != nil, MaxAge: int(sessionMaxLife.Seconds())})
|
http.SetCookie(w, &http.Cookie{Name: sessionCookie, Value: s.Token, Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode, Secure: r.TLS != nil, MaxAge: int(sessionMaxLife.Seconds())})
|
||||||
log.Printf("auth: первичная настройка: создан администратор %s с %s", u.Login, ip)
|
log.Printf("auth: первичная настройка: создан администратор %s с %s", u.Login, ip)
|
||||||
|
// ключ продукта, если его не вводили на сервере: активируем сразу, чтобы модули и обновления заработали
|
||||||
|
if d.NeedKey && d.ProductKey != "" && globalRC != nil {
|
||||||
|
actx, acancel := context.WithTimeout(r.Context(), 30*time.Second)
|
||||||
|
err := LicenseActivate(actx, globalRC, d.ProductKey)
|
||||||
|
acancel()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("auth: первичная настройка: ключ продукта не принят: %v", err)
|
||||||
|
http.Redirect(w, r, "/admin/setup?flash="+url.QueryEscape("Лицензия: ключ продукта не принят — "+err.Error()+". Введите его в разделе «Лицензия».")+"#sec-license", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Redirect(w, r, "/admin/setup?flash="+url.QueryEscape("Лицензия активирована. Дальше — подключение к базе DEPO, затем реквизиты НРД (кнопка «Определить из DEPO»)."), http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if d.NeedKey {
|
||||||
|
http.Redirect(w, r, "/admin/setup?flash="+url.QueryEscape("Администратор создан. Лицензия ещё не активирована — введите ключ продукта в разделе «Лицензия», иначе модули и обновления недоступны.")+"#sec-license", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
http.Redirect(w, r, "/admin/setup", http.StatusSeeOther)
|
http.Redirect(w, r, "/admin/setup", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -123,6 +146,7 @@ code{font-size:12px}
|
|||||||
<label>Имя</label><input type="text" name="name" value="{{.Name}}" autocomplete="name">
|
<label>Имя</label><input type="text" name="name" value="{{.Name}}" autocomplete="name">
|
||||||
<label>Пароль, не короче 10 символов</label><input type="password" name="password" minlength="10" autocomplete="new-password" required>
|
<label>Пароль, не короче 10 символов</label><input type="password" name="password" minlength="10" autocomplete="new-password" required>
|
||||||
<label>Повтор пароля</label><input type="password" name="password2" minlength="10" autocomplete="new-password" required>
|
<label>Повтор пароля</label><input type="password" name="password2" minlength="10" autocomplete="new-password" required>
|
||||||
|
{{if .NeedKey}}<label>Ключ продукта (BJ-XXXX-XXXX-XXXX) — если не вводили на сервере; можно позже в «Настройка → Лицензия»</label><input type="text" name="product_key" value="{{.ProductKey}}" placeholder="BJ-XXXX-XXXX-XXXX" autocomplete="off" style="text-transform:uppercase">{{end}}
|
||||||
<button type="submit">Создать администратора</button>
|
<button type="submit">Создать администратора</button>
|
||||||
{{if .Error}}<div class="err">{{.Error}}</div>{{end}}
|
{{if .Error}}<div class="err">{{.Error}}</div>{{end}}
|
||||||
<div class="hint">Код потерян или истёк: на сервере выполните <code>sudo bj-installer first-run-code</code>.</div>
|
<div class="hint">Код потерян или истёк: на сервере выполните <code>sudo bj-installer first-run-code</code>.</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user