package main import ( "encoding/json" "fmt" "os" "os/exec" "path/filepath" "strings" ) // --------------------------------------------------------------------- // // Хелперы // --------------------------------------------------------------------- // // runCmd — запускает команду, прокидывает stdout/stderr построчно в log. // Возвращает ошибку с последними строками stderr для удобства отображения. func runCmd(logFn func(string), name string, args ...string) error { logFn(fmt.Sprintf("$ %s %s", name, strings.Join(args, " "))) cmd := exec.Command(name, args...) out, err := cmd.CombinedOutput() for _, line := range strings.Split(strings.TrimRight(string(out), "\n"), "\n") { if line != "" { logFn(line) } } if err != nil { return fmt.Errorf("%s: %w", name, err) } return nil } // writeFileIfChanged — пишет файл только если содержимое отличается. Возвращает // true если файл был создан/изменён (для решения «нужен ли daemon-reload»). func writeFileIfChanged(path string, content string, mode os.FileMode) (bool, error) { existing, err := os.ReadFile(path) if err == nil && string(existing) == content { return false, nil } if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return false, err } if err := os.WriteFile(path, []byte(content), mode); err != nil { return false, err } return true, nil } // --------------------------------------------------------------------- // // Шаги // --------------------------------------------------------------------- // func stepInstallDeps(s *State, log func(string)) (StepStatus, error) { log("Обновляю apt-кеш...") if err := runCmd(log, "apt-get", "update", "-qq"); err != nil { return StepFailed, err } deps := []string{ "libgtk-3-0", "libpcsclite1", "libccid", "pcscd", "libcurl4", "libkrb5-3", "libgssapi-krb5-2", "libsasl2-modules", "libsasl2-modules-gssapi-mit", "execstack", "p7zip-full", } if hasAPTPackage("libldap-2.4-2") { deps = append(deps, "libldap-2.4-2") } else { deps = append(deps, "libldap-2.5-0") log("libldap-2.4-2 не найден → ставлю 2.5-0, для zpki будет --force-depends") } args := append([]string{"install", "-y", "--no-install-recommends"}, deps...) if err := runCmd(log, "apt-get", args...); err != nil { return StepFailed, err } return StepDone, nil } func stepInstallValidataDebs(s *State, log func(string)) (StepStatus, error) { zpki, _ := filepath.Glob(filepath.Join(s.artifactsDir, "ClientL_Other", "zpki-*.amd64.deb")) zsdk, _ := filepath.Glob(filepath.Join(s.artifactsDir, "ClientL_Other", "zsdk-*.amd64.deb")) if len(zpki) == 0 { return StepFailed, fmt.Errorf("zpki-*.amd64.deb не найден в %s/ClientL_Other/", s.artifactsDir) } useForce := !hasAPTPackage("libldap-2.4-2") for _, deb := range append(zpki, zsdk...) { args := []string{"-i", deb} if useForce { args = append([]string{"--force-depends"}, args...) } if err := runCmd(log, "dpkg", args...); err != nil { return StepFailed, err } } if _, err := os.Stat("/opt/Validata/VDCSP/lib/amd64"); err != nil { return StepFailed, fmt.Errorf("/opt/Validata не появился после установки") } return StepDone, nil } func stepExecstack(s *State, log func(string)) (StepStatus, error) { target := "/opt/Validata/VDCSP/lib/amd64/libvdcsp.so" // Проверка состояния out, err := exec.Command("execstack", "-q", target).Output() if err == nil && strings.HasPrefix(strings.TrimSpace(string(out)), "-") { log("executable-stack уже снят") return StepSkipped, nil } return StepDone, runCmd(log, "execstack", "-c", target) } func stepCreateBJUser(s *State, log func(string)) (StepStatus, error) { if _, err := exec.LookPath("id"); err == nil { if exec.Command("id", "bj").Run() == nil { log("Пользователь bj уже существует") } else { if err := runCmd(log, "useradd", "--system", "--create-home", "--home-dir", "/var/lib/bj", "--shell", "/bin/bash", "bj"); err != nil { return StepFailed, err } } } dirs := []struct { Path string Mode os.FileMode }{ {"/var/lib/bj/usb", 0o755}, {"/var/lib/bj/.Validata", 0o700}, {"/var/lib/bj/.Validata/vdkeys", 0o700}, {"/var/lib/bj/profiles", 0o755}, {"/var/log/bj", 0o755}, {"/var/lib/bj/.bj", 0o700}, } for _, d := range dirs { if err := os.MkdirAll(d.Path, d.Mode); err != nil { return StepFailed, err } } return StepDone, runCmd(log, "chown", "-R", "bj:bj", "/var/lib/bj", "/var/log/bj") } func stepPcscdDropin(s *State, log func(string)) (StepStatus, error) { const dropin = `[Unit] Requires= After= Sockets= [Service] ExecStart= ExecStart=/usr/sbin/pcscd --foreground ` changed, err := writeFileIfChanged("/etc/systemd/system/pcscd.service.d/no-autoexit.conf", dropin, 0o644) if err != nil { return StepFailed, err } if !changed { log("Drop-in уже актуален") return StepSkipped, nil } log("Создан /etc/systemd/system/pcscd.service.d/no-autoexit.conf") return StepDone, nil } func stepBJCryptoDropins(s *State, log func(string)) (StepStatus, error) { files := map[string]string{ "/etc/systemd/system/bj-crypto.service.d/validata-paths.conf": `[Service] WorkingDirectory=/opt/Validata/VDCSP/etc ReadWritePaths=/opt/Validata/VDCSP/etc ReadWritePaths=/var/lib/bj `, "/etc/systemd/system/bj-crypto.service.d/usb-access.conf": `[Service] ReadOnlyPaths=/media ReadOnlyPaths=/var/lib/bj/usb `, "/etc/systemd/system/bj-crypto.service.d/share-crysvc.conf": `[Service] PrivateTmp=true BindPaths=/tmp/.crysvc.sock:/tmp/.crysvc.sock `, } for path, content := range files { if _, err := writeFileIfChanged(path, content, 0o644); err != nil { return StepFailed, err } } return StepDone, nil } func stepBJServerDropin(s *State, log func(string)) (StepStatus, error) { const dropin = `[Service] ReadWritePaths=/opt/Validata/VDCSP/etc ` _, err := writeFileIfChanged("/etc/systemd/system/bj-server.service.d/pki1conf.conf", dropin, 0o644) if err != nil { return StepFailed, err } return StepDone, nil } func stepSPKIIni(s *State, log func(string)) (StepStatus, error) { const path = "/opt/Validata/VDCSP/etc/spki.ini" if _, err := os.Stat(path); err == nil { log("Файл уже существует") return StepSkipped, nil } const content = `[store] count = 0 [Parameters] PkiLdapTimeout = 10 PkiHttpTimeout = 60 ` if err := os.WriteFile(path, []byte(content), 0o644); err != nil { return StepFailed, err } return StepDone, nil } func stepPKI1Prep(s *State, log func(string)) (StepStatus, error) { const path = "/opt/Validata/VDCSP/etc/pki1.conf" if _, err := os.Stat(path); err != nil { log("Файл pki1.conf отсутствует — Валидата создаст при первом запуске") return StepSkipped, nil } if err := runCmd(log, "chgrp", "bj", path); err != nil { return StepFailed, err } if err := runCmd(log, "chmod", "g+w", path); err != nil { return StepFailed, err } existing, _ := os.ReadFile(path) if !strings.Contains(string(existing), "# --- bj-server: BEGIN ---") { appended := string(existing) + "\n# --- bj-server: BEGIN ---\n# Секции профилей дописываются bj-server при импорте через /admin/setup.\n# --- bj-server: END ---\n" if err := os.WriteFile(path, []byte(appended), 0o664); err != nil { return StepFailed, err } } return StepDone, nil } func stepUSBMount(s *State, log func(string)) (StepStatus, error) { files := map[string]string{ "/etc/udev/rules.d/99-bj-usb.rules": `# Авто-mount USB-флешек в /var/lib/bj/usb/ с владельцем bj. ACTION=="add", KERNEL=="sd?[0-9]", SUBSYSTEMS=="usb", \ ENV{ID_FS_TYPE}!="", \ ENV{SYSTEMD_WANTS}="bj-usb-mount@$env{ID_FS_UUID}.service" ACTION=="remove", KERNEL=="sd?[0-9]", SUBSYSTEMS=="usb", \ ENV{ID_FS_TYPE}!="", \ ENV{SYSTEMD_WANTS}="bj-usb-umount@$env{ID_FS_UUID}.service" `, "/etc/systemd/system/bj-usb-mount@.service": `[Unit] Description=Mount USB %i to /var/lib/bj/usb/%i for bj DefaultDependencies=no After=local-fs.target [Service] Type=oneshot RemainAfterExit=yes ExecStart=/usr/bin/bash -c 'mkdir -p /var/lib/bj/usb/%i && /usr/bin/mount -o uid=$(id -u bj),gid=$(id -g bj),fmask=0133,dmask=0022 UUID=%i /var/lib/bj/usb/%i' ExecStop=/usr/bin/umount /var/lib/bj/usb/%i || true `, "/etc/systemd/system/bj-usb-umount@.service": `[Unit] Description=Umount USB %i from /var/lib/bj/usb/%i DefaultDependencies=no [Service] Type=oneshot ExecStart=/usr/bin/bash -c '/usr/bin/umount /var/lib/bj/usb/%i 2>/dev/null; /usr/bin/rmdir /var/lib/bj/usb/%i 2>/dev/null; true' `, } anyChanged := false for path, content := range files { ch, err := writeFileIfChanged(path, content, 0o644) if err != nil { return StepFailed, err } anyChanged = anyChanged || ch } if anyChanged { _ = runCmd(log, "udevadm", "control", "--reload-rules") _ = runCmd(log, "udevadm", "trigger") } return StepDone, nil } func stepInstallBJServer(s *State, log func(string)) (StepStatus, error) { src := filepath.Join(s.artifactsDir, "bj-server") if _, err := os.Stat(src); err != nil { return StepSkipped, nil // нет бинаря — может ставится через rpm/deb } if err := os.MkdirAll("/opt/bj", 0o755); err != nil { return StepFailed, err } if err := runCmd(log, "install", "-o", "bj", "-g", "bj", "-m", "0755", src, "/opt/bj/bj-server"); err != nil { return StepFailed, err } return StepDone, nil } func stepInstallCryptoJar(s *State, log func(string)) (StepStatus, error) { src := filepath.Join(s.artifactsDir, "crypto-service.jar") if _, err := os.Stat(src); err != nil { return StepSkipped, nil } if err := os.MkdirAll("/opt/bj", 0o755); err != nil { return StepFailed, err } if err := runCmd(log, "install", "-o", "bj", "-g", "bj", "-m", "0644", src, "/opt/bj/crypto-service.jar"); err != nil { return StepFailed, err } return StepDone, nil } func stepSystemdUnits(s *State, log func(string)) (StepStatus, error) { units := map[string]string{ "/etc/systemd/system/bj-crypto.service": `[Unit] Description=Bridge-and-Join-s — Crypto sidecar (Java + Валидата Клиент L) Before=bj-server.service After=network-online.target pcscd.service Wants=network-online.target [Service] Type=simple User=bj Group=bj RuntimeDirectory=bj RuntimeDirectoryMode=0750 Environment=BJ_CRYPTO_SOCKET=/run/bj/crypto.sock Environment=BJ_CRYPTO_PROVIDER=validata Environment=LD_LIBRARY_PATH=/opt/Validata/VDCSP/lib/amd64 ExecStart=/usr/bin/java -Djava.library.path=/opt/Validata/VDCSP/lib/amd64 -jar /opt/bj/crypto-service.jar Restart=on-failure RestartSec=5 StandardOutput=append:/var/log/bj/crypto-service.log StandardError=append:/var/log/bj/crypto-service.err NoNewPrivileges=true ProtectSystem=strict ProtectHome=true ReadWritePaths=/run/bj /var/log/bj PrivateTmp=true [Install] WantedBy=multi-user.target `, "/etc/systemd/system/bj-server.service": `[Unit] Description=Bridge-and-Join-s — единый сервис M2M-переводов After=network-online.target bj-crypto.service Wants=network-online.target [Service] Type=simple User=bj Group=bj WorkingDirectory=/var/lib/bj ExecStart=/opt/bj/bj-server Restart=on-failure RestartSec=5 Environment=BJ_HTTP_ADDR=:8080 Environment=BJ_SETUP_PATH=/var/lib/bj/.bj/setup.json ProtectSystem=strict ProtectHome=true ReadWritePaths=/var/lib/bj /var/log/bj [Install] WantedBy=multi-user.target `, } for path, content := range units { if _, err := writeFileIfChanged(path, content, 0o644); err != nil { return StepFailed, err } } return StepDone, runCmd(log, "systemctl", "daemon-reload") } func stepInstallISH(s *State, log func(string)) (StepStatus, error) { matches, _ := filepath.Glob(filepath.Join(s.artifactsDir, "ish", "igate_*.deb")) if len(matches) == 0 { log("Дистрибутив ИШ не найден — пропускаю (можно установить позже)") return StepSkipped, nil } if err := runCmd(log, "dpkg", "-i", matches[0]); err != nil { // допустим, что зависимости подтянутся _ = runCmd(log, "apt-get", "-f", "install", "-y") if err := runCmd(log, "dpkg", "-i", matches[0]); err != nil { return StepFailed, err } } return StepDone, nil } func stepSaveConfig(s *State, log func(string)) (StepStatus, error) { cfg := s.Snapshot().Config if cfg.OrgINN == "" && cfg.AdminEmail == "" && cfg.LicenseKey == "" { return StepSkipped, nil } b, err := json.MarshalIndent(cfg, "", " ") if err != nil { return StepFailed, err } if err := os.MkdirAll("/var/lib/bj/.bj", 0o700); err != nil { return StepFailed, err } if err := os.WriteFile("/var/lib/bj/.bj/setup.json", b, 0o600); err != nil { return StepFailed, err } return StepDone, runCmd(log, "chown", "-R", "bj:bj", "/var/lib/bj/.bj") } func stepStartServices(s *State, log func(string)) (StepStatus, error) { // disable+stop socket activation для pcscd _ = runCmd(log, "systemctl", "stop", "pcscd.socket") _ = runCmd(log, "systemctl", "disable", "pcscd.socket") for _, svc := range []string{"pcscd", "bj-crypto", "bj-server"} { if err := runCmd(log, "systemctl", "enable", svc); err != nil { return StepFailed, err } if err := runCmd(log, "systemctl", "restart", svc); err != nil { return StepFailed, err } } return StepDone, nil } func stepHealthCheck(s *State, log func(string)) (StepStatus, error) { var bad []string for _, svc := range []string{"pcscd", "vdcrysvc", "bj-crypto", "bj-server"} { if err := exec.Command("systemctl", "is-active", "--quiet", svc).Run(); err != nil { bad = append(bad, svc) } else { log(svc + ": active") } } if len(bad) > 0 { return StepFailed, fmt.Errorf("сервисы не запустились: %s", strings.Join(bad, ", ")) } return StepDone, nil } // hasAPTPackage — проверяет наличие пакета в apt-cache (доступен ли для установки). func hasAPTPackage(name string) bool { out, err := exec.Command("apt-cache", "show", name).CombinedOutput() if err != nil { return false } return strings.Contains(string(out), "Package: "+name) }