Установка: приветственный экран консоли (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
|
||||
lic *license.License
|
||||
setupCode string // одноразовый код первичной настройки (итоговый экран)
|
||||
bundle string // локальный релиз (образ ISO): ядро без ключа продукта
|
||||
shown bool
|
||||
}
|
||||
|
||||
@@ -99,6 +100,7 @@ func cmdInstall(args []string) {
|
||||
fl := flag.NewFlagSet("install", flag.ExitOnError)
|
||||
in := &installer{out: newConsole()}
|
||||
fl.StringVar(&in.code, "code", "", "ключ продукта (код активации)")
|
||||
fl.StringVar(&in.bundle, "bundle", "", "каталог с релизом (manifest.json и файлы) — установка ядра без ключа продукта, ключ вводится в браузере")
|
||||
fl.StringVar(&in.server, "server", "https://lic.zetit.ru", "сервер лицензий и обновлений")
|
||||
fl.StringVar(&in.channel, "channel", "stable", "канал обновлений")
|
||||
fl.BoolVar(&in.dryRun, "dry-run", false, "только проверить систему")
|
||||
@@ -136,7 +138,7 @@ func (in *installer) run() error {
|
||||
if in.root == "" && !in.dryRun && os.Geteuid() != 0 {
|
||||
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/)")
|
||||
}
|
||||
all := []step{
|
||||
@@ -356,6 +358,9 @@ func machineHash() string {
|
||||
}
|
||||
|
||||
func (in *installer) stepLicense() (string, error) {
|
||||
if strings.TrimSpace(in.code) == "" && in.bundle != "" {
|
||||
return "пропущена: ключ продукта вводится в браузере на первой странице настройки", nil
|
||||
}
|
||||
setup := in.loadSetup()
|
||||
lic := section(setup, "license")
|
||||
installID, _ := lic["install_id"].(string)
|
||||
@@ -516,6 +521,9 @@ func unitTemplate(unit string) bool {
|
||||
}
|
||||
|
||||
func (in *installer) stepComponents() (string, error) {
|
||||
if in.bundle != "" {
|
||||
return in.componentsFromBundle()
|
||||
}
|
||||
if err := in.ensureToken(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -584,6 +592,77 @@ func (in *installer) stepComponents() (string, error) {
|
||||
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) {
|
||||
var files []string
|
||||
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 {
|
||||
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-файл
|
||||
for name := range release.LoadInstalled(in.P(release.InstalledFile)) {
|
||||
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))
|
||||
code := digits[:4] + "-" + digits[4:]
|
||||
url := setupURLs()
|
||||
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)
|
||||
in.refreshWelcome()
|
||||
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) {
|
||||
in := &installer{out: newConsole()}
|
||||
if os.Geteuid() != 0 {
|
||||
@@ -860,6 +947,9 @@ func (c *console) done(code string, l *license.License) {
|
||||
} else {
|
||||
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 {
|
||||
fmt.Fprintf(c.w, "\n Лицензия: %s, план %s, ключ до %s, модули: %s\n", l.Tenant, l.Plan, l.ExpiresAt.Local().Format("02.01.2006"), featuresText(l))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user