9737c787f9
Инфраструктура M2M (живой обмен с НРД через ИШ): - обработка M2MTransferResponse: ERROR(M2Mxx) → заявка Отклонена, сохранение ответа; INFO → ждём Decision; идемпотентность поллера - fallback-корреляция ответов с нулевым GUID (M2M14/M2M17) по FIFO - сырой XML ответа НРД в карточке заявки (для пересылки в ТП) - тестовый пакет роботу приведён к эталону m2m_robot_samples (CostInfo=Yes, 4 бумаги, IsolationStatus, DocumentSeries=сценарий); override паспорта - редирект из теста сразу в карточку заявки Мастер установки ключа Валидаты на флешку (admin/setup/keywizard): - пошаговый: загрузка .7z+пароль → выбор флешки → запись → справочник сертификатов (CRL) → перезапуск+проверка ИШ → готово - привилегированный воркер (bj-keymedia) в host-namespace через файл-обмен, bj-server остаётся в песочнице - сохранение структуры профиля архива (spr<N>), перечисление съёмных USB Прочее: - пакет-доказательство для ТП НРД + форма регистрации участника M2M - эталонные образцы робота (DOC/m2m_robot_samples) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
449 lines
14 KiB
Go
449 lines
14 KiB
Go
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/<UUID> с владельцем 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)
|
||
}
|