Загрузка данных


#!/usr/bin/env bash
set -uo pipefail

# ОСнова (ОСОн, АО «НППКТ») 2.14 / 3.1 — local-setup «всё в одном», идемпотентно.
#
# ⚠️ ГЛАВНЫЙ ПРИНЦИП: пакеты берём ТОЛЬКО из РОДНЫХ установочных дисков ОСновы
#    (disk1/disk2/devel — те, что выдал вендор и что примонтированы в Proxmox как
#    CD/DVD). НИКАКИХ Debian-репозиториев: они тянут чужие libc/libssl/zlib других
#    версий → ломают пакетную базу ОСновы и не имеют подписей ЗПС. Всё, что нужно
#    для стенда, есть на дисках; если пакета на смонтированных дисках нет — это
#    сигнал подключить нужный диск (обычно «Комплект разработчика»), а НЕ тянуть
#    из Debian.
#
# Запуск ПОД ОБЫЧНЫМ пользователем (НЕ root — pyenv/venv ставятся в $HOME):
#     bash osnova_local_setup.sh
#
# ЧТО ТАКОЕ ОСНОВА (важно для скрипта):
#   - дебиан-подобная: apt/dpkg, обычные *_amd64.deb; 2.14 — база Debian 10 (onyx,
#     OpenSSL 1.1.1 → Python 3.9.6 собирается), 3.1 — база Debian 12 (onyx3, OpenSSL 3.x
#     → §6 сам выберет PYVER=3.9.20);
#   - рабочий стол KDE Plasma → активный DM = sddm; продукту lightdm нужен только
#     УСТАНОВЛЕННЫМ (приедет зависимостью .deb) — §10;
#   - ⚠️ ЗПС на базе IMA подписывает бинарники И скрипты: при enforce pyenv-Python/venv
#     и наши скрипты не запустятся (§0.5 проверяет, на стенде ЗПС выключить);
#   - репозиторий = установочные диски (§3). Диски вендор отдал как ISO — в Proxmox
#     они подключены как CD/DVD (ide0/ide2 → /dev/sr0,/dev/sr1 в госте).

TESTER="${TESTER:-$(id -un)}"
PYVER="${PYVER:-}"                          # пусто = автовыбор по OpenSSL (§6)
PROJECT_GIT="${PROJECT_GIT:-https://scm.aktivco.ru/rutoken/dev/qa-dev/rtlogon_pytests.git}"
PROJECT_DIR="${PROJECT_DIR:-$HOME/rtlogon_pytests}"
PROJECT_REF="${PROJECT_REF:-}"
GIT_USER="${GIT_USER:-}"
GIT_PASS="${GIT_PASS:-}"
PRODUCT_URL="${PRODUCT_URL:-https://builds.aktivco.ru/new/unilogon-production/unilogon-production-143a6b8d5cc6b3527afe3ba0fcbeaa464f4e60bf.zip}"
PRODUCT_ZIP="${PRODUCT_ZIP:-}"
PRODUCT_DEB="${PRODUCT_DEB:-}"
ALLURE_VER="${ALLURE_VER:-2.29.0}"
JAVA_PKG="${JAVA_PKG:-default-jre}"
NPROC_MIN="${NPROC_MIN:-16384}"
MAKE_OPTS="${MAKE_OPTS:--j4}"
export MAKE_OPTS

SCRIPT_DIR="$(cd "$(dirname "$0")" 2>/dev/null && pwd || echo "$PWD")"
export DEBIAN_FRONTEND=noninteractive

ok(){ echo "  [OK]   $*"; }
act(){ echo "  [DO]   $*"; }
warn(){ echo "  [WARN] $*"; }
die(){ echo "ОШИБКА: $*" >&2; exit 1; }
have(){ command -v "$1" >/dev/null 2>&1; }
# apt под sudo: sudo сбрасывает окружение (env_reset), поэтому DEBIAN_FRONTEND передаём
# ЯВНО в каждом вызове, а stdin подрезаем (</dev/null) — чтобы дебконф-вопрос не повесил прогон.
apt_get(){ sudo DEBIAN_FRONTEND=noninteractive apt-get "$@" </dev/null; }
urlenc(){ local s="$1" o="" c i; for ((i=0;i<${#s};i++)); do c="${s:$i:1}"; case "$c" in [a-zA-Z0-9.~_-]) o+="$c";; *) printf -v c '%%%02X' "'$c"; o+="$c";; esac; done; printf '%s' "$o"; }

. /etc/os-release 2>/dev/null || true
echo "=== ОСнова local-setup (репозиторий = установочные диски, без Debian-репо) ==="
echo "  система: ${PRETTY_NAME:-?} (VERSION_ID=${VERSION_ID:-?}), ядро $(uname -r)"
case "${VERSION_ID:-}" in
  2*) ok "линейка 2.x (2.14, onyx: база Debian 10, OpenSSL 1.1.1)";;
  3*) ok "линейка 3.x (3.1, onyx3: база Debian 12, OpenSSL 3.x)";;
  *)  warn "VERSION_ID='${VERSION_ID:-}' не 2.x/3.x — продолжаю, различия решает OpenSSL (§6)";;
esac
[ "$(id -u)" -ne 0 ] || die "Запусти под обычным пользователем ($TESTER), НЕ root — pyenv/venv ставятся в \$HOME"

echo "== 0. sudo / привилегии =="
sudo -k 2>/dev/null || true
if sudo -n true 2>/dev/null; then
  ok "NOPASSWD sudo уже есть"
else
  act "NOPASSWD sudo не настроен — поднимаю права"
  BOOT="$(mktemp)"
  cat > "$BOOT" <<EOF
usermod -aG sudo $TESTER
[ -d /etc/sudoers.d ] || install -d -m 0755 /etc/sudoers.d
grep -qE '^[@#]includedir /etc/sudoers.d' /etc/sudoers || echo '@includedir /etc/sudoers.d' >> /etc/sudoers
echo '$TESTER ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/$TESTER
chmod 0440 /etc/sudoers.d/$TESTER
echo '$TESTER - nproc $NPROC_MIN' > /etc/security/limits.d/90-nproc.conf
EOF
  chmod 0644 "$BOOT"
  if command -v sudo >/dev/null 2>&1 && sudo -v 2>/dev/null && sudo bash "$BOOT"; then
    ok "права подняты через sudo (пароль $TESTER)"
  elif su - root -c "bash '$BOOT'"; then
    ok "права подняты через su root (пароль root)"
  else
    rm -f "$BOOT"; die "не поднять права: ни sudo, ни su root не сработали. Добавь $TESTER в sudo вручную (от root: usermod -aG sudo $TESTER)"
  fi
  rm -f "$BOOT"
  sudo -n true 2>/dev/null || die "sudo NOPASSWD всё ещё не работает — открой НОВЫЙ терминал (группа sudo подхватится при логине) и проверь: sudo -n true"
  ok "sudo NOPASSWD настроен"
fi

echo "== 0.5 ЗПС/IMA и NESS (специфика ОСновы) =="
if grep -qE 'ima_appraise=(enforce|fix)' /proc/cmdline 2>/dev/null; then
  warn "в cmdline ядра ima_appraise=$(grep -oE 'ima_appraise=[^ ]*' /proc/cmdline) — ЗПС активна!"
  warn "pyenv-Python/venv и тестовые скрипты НЕ ЗАПУСТЯТСЯ. Выключи ЗПС на время тестов"
  warn "(документация НППКТ, раздел ЗПС) — иначе прогон не поедет."
elif sudo test -s /sys/kernel/security/ima/policy 2>/dev/null; then
  warn "IMA-policy непустая — проверь режим ЗПС (audit ок, enforce — нет)"
else
  ok "ЗПС/IMA не активна (appraise не форсится)"
fi

echo "== 1. лимит nproc =="
[ -f /etc/security/limits.d/90-nproc.conf ] || \
  echo "$TESTER - nproc $NPROC_MIN" | sudo tee /etc/security/limits.d/90-nproc.conf >/dev/null
CUR="$(ulimit -u)"; case "$CUR" in unlimited) CUR=999999999;; esac
if [ "$CUR" -lt "$NPROC_MIN" ]; then
  if ulimit -u "$NPROC_MIN" 2>/dev/null; then ok "ulimit -u поднят до $(ulimit -u) на сессию"
  else warn "ulimit -u=$CUR, не поднять live — перелогинься после прогона"; NEED_RELOGIN=1; fi
else ok "ulimit -u=$CUR"; fi
ok "cgroup pids.max=$(cat /sys/fs/cgroup/user.slice/user-$(id -u).slice/pids.max 2>/dev/null || echo '?')"

echo "== 2. права \$HOME =="
BAD=""
for f in .ICEauthority .Xauthority .config .cache .dbus .local .pyenv; do
  p="$HOME/$f"
  [ -e "$p" ] && [ "$(stat -c %U "$p" 2>/dev/null)" != "$TESTER" ] && BAD="$BAD $f"
done
if [ -n "$BAD" ]; then
  warn "не от $TESTER:$BAD — чиню владельца"; sudo chown -R "$TESTER:$(id -gn)" "$HOME"; act "владелец \$HOME возвращён"
else ok "владелец \$HOME в порядке"; fi

echo "== 3. репозитории = установочные диски ОСновы =="
# Ищем примонтированные (или подключённые) CD/DVD с файловой системой ISO9660 — это диски
# ОСновы (в Proxmox ide0/ide2 → /dev/sr0,/dev/sr1). Каждый монтируем ro и регистрируем как
# apt-источник file:. Подписи ЗПС/вендора уже в связке ключей (система ставилась с этих
# дисков) → сначала пробуем БЕЗ trusted; если apt ругнётся на подпись — trusted=yes
# (диск физически доверенный, read-only, ничего чужого не тянет).
sudo install -d -m 0755 /media/osnova
DISK_SRCS=0
# кандидаты: реальные optical-устройства + любые block-устройства с iso9660
CANDS="$(ls /dev/sr[0-9] /dev/scd[0-9] 2>/dev/null)"
for dev in $CANDS; do
  [ -b "$dev" ] || continue
  blkid -o value -s TYPE "$dev" 2>/dev/null | grep -q iso9660 || {
    # некоторые приводы не отдают TYPE до монтирования — пробуем всё равно
    :
  }
  n="$(basename "$dev")"
  mp="/media/osnova/$n"
  sudo install -d -m 0755 "$mp"
  if ! mountpoint -q "$mp"; then
    sudo mount -o ro "$dev" "$mp" 2>/dev/null || { warn "$dev не смонтировался (пусто/нет диска) — пропускаю"; continue; }
  fi
  # apt-структура диска: dists/<suite>/... — suite обычно 'onyx' (или 'onyx3')
  if [ ! -d "$mp/dists" ]; then
    warn "$dev ($mp) без dists/ — это не apt-репозиторий ОСновы, пропускаю"
    continue
  fi
  SUITE="$(ls "$mp/dists" 2>/dev/null | grep -viE 'stable|testing' | head -1)"
  [ -n "$SUITE" ] || SUITE="$(ls "$mp/dists" 2>/dev/null | head -1)"
  LABEL="$(blkid -o value -s LABEL "$dev" 2>/dev/null)"
  echo "deb file:${mp} ${SUITE} main contrib non-free" | sudo tee "/etc/apt/sources.list.d/osnova-${n}.list" >/dev/null
  # fstab (nofail,ro) — чтобы диск был доступен и после ребута (для join-скриптов apt)
  if ! grep -q " ${mp} " /etc/fstab 2>/dev/null; then
    echo "${dev}  ${mp}  iso9660  ro,nofail,x-systemd.automount  0  0" | sudo tee -a /etc/fstab >/dev/null
  fi
  act "диск ${n}${LABEL:+ [$LABEL]} → file:${mp} (suite=${SUITE})"
  DISK_SRCS=$((DISK_SRCS+1))
done
# Родные cdrom:-строки заменяем нашими file: (cdrom:-метод apt интерактивен — просит
# «вставьте диск», что и вешало прогон). Комментируем с бэкапом.
if grep -q '^deb cdrom:' /etc/apt/sources.list 2>/dev/null; then
  sudo sed -i.bak 's/^deb cdrom:/# deb cdrom:/' /etc/apt/sources.list
  act "cdrom:-строки закомментированы (бэкап /etc/apt/sources.list.bak) — вместо них file:"
fi
[ "$DISK_SRCS" -gt 0 ] || die "не найдено ни одного установочного диска ОСновы (примонтированного ISO9660 с dists/).
  Подключи диски в Proxmox: Hardware → CD/DVD Drive → образ onyx-2.14-disk1/disk2 (и devel, если нужен),
  затем повтори скрипт. Проверить вручную:  ls /dev/sr* ; sudo mount -o ro /dev/sr0 /mnt ; ls /mnt/dists"
# добить возможные недонастроенные пакеты с прошлых прерванных прогонов
sudo DEBIAN_FRONTEND=noninteractive dpkg --configure -a </dev/null 2>/dev/null || true
apt_get update || warn "apt-get update с ошибкой — смотри вывод (подпись диска? см. ниже)"
# если update упал на подписи (NO_PUBKEY / not signed) — перевести file:-источники на trusted=yes
if apt_get update 2>&1 | grep -qiE 'NO_PUBKEY|is not signed|GPG error'; then
  warn "диск без валидной подписи в связке ключей — перевожу file: на [trusted=yes]"
  for lst in /etc/apt/sources.list.d/osnova-*.list; do
    [ -f "$lst" ] && sudo sed -i 's#^deb file:#deb [trusted=yes] file:#' "$lst"
  done
  apt_get update || warn "update всё ещё с ошибкой"
fi
ok "репозитории с дисков подключены ($DISK_SRCS шт.)"

echo "== 4. build-deps (ТОЛЬКО с дисков; чего нет — нужен диск «Комплект разработчика») =="
# Заранее отвечаем на дебконф-вопрос libc (на всякий; с родными дисками libc не обновляется).
echo 'libc6 libraries/restart-without-asking boolean true' | sudo debconf-set-selections 2>/dev/null || true
# Фильтруем список по тому, что реально ЕСТЬ на дисках: apt-get install падает целиком,
# если хоть один пакет не найден, — поэтому ставим только доступные, а недостающие копим.
WANT="make gcc g++ patch build-essential \
  libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev \
  libncurses-dev libncurses5-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev tk-dev \
  libgdbm-dev libgdbm-compat-dev uuid-dev \
  wget curl git zip unzip xz-utils $JAVA_PKG"
AVAIL=""; MISS=""
for p in $WANT; do
  if dpkg -s "$p" >/dev/null 2>&1; then continue; fi          # уже стоит
  if apt-cache show "$p" >/dev/null 2>&1; then AVAIL="$AVAIL $p"; else MISS="$MISS $p"; fi
done
if [ -n "$AVAIL" ]; then
  act "ставлю с дисков:$AVAIL"
  # shellcheck disable=SC2086
  apt_get install -y $AVAIL || warn "часть пакетов не встала — смотри вывод"
else
  ok "всё нужное уже установлено"
fi
if [ -n "$MISS" ]; then
  warn "НЕТ на подключённых дисках:$MISS"
  echo "  Это пакеты с диска «Комплект разработчика» (devel). Подключи его ISO в Proxmox"
  echo "  (Hardware → CD/DVD → образ onyx-*-devel*.iso) и повтори скрипт — они встанут в"
  echo "  родных osnova-версиях. Debian-репозитории НЕ подключаем (сломают пакетную базу)."
fi
# гейт: без инструментов и критичного минимума -dev Python/pip не собрать
for t in git gcc make wget unzip; do
  have "$t" || die "«$t» нет — на подключённых дисках его не оказалось. Нужен devel-диск (см. выше)."
done
for p in libssl-dev zlib1g-dev libffi-dev; do
  dpkg -s "$p" >/dev/null 2>&1 || die "критичный $p не установлен — нужен devel-диск ОСновы (см. выше). Без него Python/pip не соберутся."
done
ok "build-deps обработаны (с дисков)"

echo "== 5. pyenv =="
if [ -d "$HOME/.pyenv" ]; then ok "pyenv есть"
else git clone https://github.com/pyenv/pyenv.git "$HOME/.pyenv" && act "pyenv склонирован"; fi
if grep -q 'pyenv init' "$HOME/.bashrc" 2>/dev/null; then
  sed -i '/pyenv init/d' "$HOME/.bashrc"; warn "убрал 'eval pyenv init' из ~/.bashrc (рекурсивный форк)"
fi
if ! grep -q 'PYENV_ROOT' "$HOME/.bashrc" 2>/dev/null; then
  { echo 'export PYENV_ROOT="$HOME/.pyenv"'
    echo 'export PATH="$PYENV_ROOT/bin:$PYENV_ROOT/shims:$PATH"'; } >> "$HOME/.bashrc"
  act "PYENV_ROOT/PATH добавлены в ~/.bashrc (без eval init)"
fi
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PYENV_ROOT/shims:$PATH"

echo "== 6. Python =="
if [ -z "$PYVER" ]; then
  case "$(openssl version 2>/dev/null | awk '{print $2}')" in
    3.*) PYVER="3.9.20"; act "OpenSSL 3.x (ОСнова 3.1) → PYVER=3.9.20";;
    *)   PYVER="3.9.6";  act "OpenSSL 1.x (ОСнова 2.14) → PYVER=3.9.6";;
  esac
fi
if pyenv versions --bare 2>/dev/null | grep -qx "$PYVER"; then ok "Python $PYVER уже собран"
else act "сборка Python $PYVER (MAKE_OPTS=$MAKE_OPTS)"; pyenv install -s "$PYVER" \
  || die "Python $PYVER не собрался — если ошибка в _ssl, перезапусти PYVER=3.9.20 (OpenSSL 3); иначе не хватает -dev (нужен devel-диск)"; fi
pyenv rehash 2>/dev/null || true

echo "== 7. проект =="
export GIT_TERMINAL_PROMPT=1
if [ ! -d "$PROJECT_DIR/.git" ]; then
  CLONE_URL="$PROJECT_GIT"
  if [ -n "$GIT_USER" ]; then
    CLONE_URL="https://$(urlenc "$GIT_USER")@${PROJECT_GIT#https://}"
    if [ -n "$GIT_PASS" ]; then
      AP="$(mktemp)"; printf '#!/bin/sh\nprintf %%s %q\n' "$GIT_PASS" > "$AP"; chmod +x "$AP"; export GIT_ASKPASS="$AP"
      act "клон под $GIT_USER (пароль через askpass)"
    fi
  else warn "git спросит логин/пароль GitLab (или задай GIT_USER=.. GIT_PASS=..)"; fi
  git clone "$CLONE_URL" "$PROJECT_DIR" || die "git clone не прошёл (логин/пароль или сеть)"
  [ -n "${AP:-}" ] && rm -f "$AP"
  act "проект склонирован"
fi
cd "$PROJECT_DIR" || die "нет $PROJECT_DIR"
[ -n "$PROJECT_REF" ] && git checkout "$PROJECT_REF"
ok "$PROJECT_DIR"

echo "== 8. venv + requirements =="
pyenv local "$PYVER"
[ -d venv ] || python -m venv venv
source venv/bin/activate
python -m pip install -q --upgrade pip
python -m pip install -q -r requirements.txt || warn "pip install -r requirements.txt с ошибкой"
ok "venv готов"

echo "== 9. SDK =="
python download_deps.py || warn "download_deps.py не отработал (доступ к SDK?)"

echo "== 10. продукт rtlogon =="
if dpkg -l 2>/dev/null | grep -qiE 'rutokenlogon|unilogon'; then
  ok "уже установлен: $(dpkg -l | grep -iE 'rutokenlogon|unilogon' | awk '{print $2"="$3}' | head -1)"
else
  DEB="$PRODUCT_DEB"; ZIP="$PRODUCT_ZIP"
  [ -z "$ZIP" ] && ZIP="$(ls "$SCRIPT_DIR"/unilogon-production-*.zip "$PWD"/unilogon-production-*.zip 2>/dev/null | head -1)"
  if [ -z "$DEB" ] && [ -z "$ZIP" ]; then
    TMP="$(mktemp -d)"; act "локального zip нет, качаю $PRODUCT_URL"
    wget -q "$PRODUCT_URL" -O "$TMP/p.zip" && ZIP="$TMP/p.zip" || warn "zip не скачался (фаервол?) — положи рядом или задай PRODUCT_DEB=путь"
  fi
  if [ -z "$DEB" ] && [ -n "$ZIP" ]; then
    act "распаковываю $(basename "$ZIP")"
    EX="$(mktemp -d)"; unzip -q "$ZIP" -d "$EX"
    find "$EX" -maxdepth 4 \( -name '*.tar.gz' -o -name '*.tgz' -o -name '*.tar' -o -name '*.zip' \) \
      -exec sh -c 'd=$(dirname "$1"); case "$1" in *.zip) unzip -qo "$1" -d "$d";; *) tar -xf "$1" -C "$d";; esac' _ {} \; 2>/dev/null
    ARCH="$(dpkg --print-architecture 2>/dev/null || echo amd64)"
    # ОСнова = обычный debian-family пакет; берём _${ARCH}.deb БЕЗ astra/alt-тега и БЕЗ grdcontrol
    DEB="$(find "$EX" -name "*_${ARCH}.deb" | grep -viE 'astra|alt|grdcontrol|guardant' | head -1)"
    [ -z "$DEB" ] && DEB="$(find "$EX" -name '*.deb' | grep -viE 'astra|alt|grdcontrol|guardant|arm64|i386' | head -1)"
    if [ -z "$DEB" ]; then
      warn "debian-family .deb под ${ARCH} в архиве не найден. Все .deb внутри:"; find "$EX" -name '*.deb' | sed 's/^/      /'
      warn "задай нужный явно: PRODUCT_DEB=путь"
    fi
  fi
  if [ -n "$DEB" ] && [ -f "$DEB" ]; then
    act "ставлю $(basename "$DEB")"
    # KDE Plasma → активный DM = sddm. Продукту lightdm нужен УСТАНОВЛЕННЫМ (приедет
    # зависимостью .deb), активным быть не обязан → DM по умолчанию НЕ трогаем.
    # ⚠️ Зависимости продукта (lightdm, pcsc, Qt) apt возьмёт С ДИСКОВ — они там есть.
    case "${DISPLAY_MANAGER:-keep}" in
      lightdm|sddm)
        sudo debconf-set-selections <<< "sddm    shared/default-x-display-manager select ${DISPLAY_MANAGER}" 2>/dev/null || true
        sudo debconf-set-selections <<< "lightdm shared/default-x-display-manager select ${DISPLAY_MANAGER}" 2>/dev/null || true
        echo "/usr/sbin/${DISPLAY_MANAGER}" | sudo tee /etc/X11/default-display-manager >/dev/null
        act "DM принудительно выставлен: ${DISPLAY_MANAGER} (нужен reboot)"
        ;;
    esac
    apt_get install -y "$DEB" && act "установлен $(basename "$DEB")" \
      || { sudo dpkg -i "$DEB"; apt_get -f install -y; } \
      || warn "deb не встал — доустанови зависимости (если пакета нет на дисках — нужен devel/extras-диск)"
    ACT_DM="$(basename "$(cat /etc/X11/default-display-manager 2>/dev/null)" 2>/dev/null)"
    echo "  активный DM: ${ACT_DM:-?}; lightdm установлен: $(dpkg -s lightdm >/dev/null 2>&1 && echo да || echo НЕТ)"
    dpkg -s lightdm >/dev/null 2>&1 || warn "lightdm НЕ установлен — тесты с --use-system-gui no по /etc/lightdm будут скипаться"
  fi
fi
CLI="$(ls /opt/aktivco/*/app/*logon-cli 2>/dev/null | head -1)"
[ -n "$CLI" ] && "$CLI" --version 2>/dev/null && ok "cli: $CLI"

echo "== 11. Allure =="
if have allure; then ok "allure стоит: $(allure --version 2>/dev/null)"
else
  TGZ="allure-commandline-${ALLURE_VER}.tgz"
  if ( cd /tmp && wget -q "https://repo1.maven.org/maven2/io/qameta/allure/allure-commandline/${ALLURE_VER}/${TGZ}" \
        && sudo tar -zxf "$TGZ" -C /opt/ && sudo ln -sf "/opt/allure-${ALLURE_VER}/bin/allure" /usr/local/bin/allure ); then
    act "Allure $ALLURE_VER установлен"; allure --version 2>/dev/null || warn "allure не стартовал — проверь java -version"
  else warn "Allure не скачался (Maven Central недоступен?) — поставь вручную"; fi
fi

echo
echo "=== Готово ==="
[ "${NEED_RELOGIN:-0}" = 1 ] && echo "ПЕРЕЛОГИНЬСЯ (или reboot) — поднимется nproc."
echo "Локальные тесты:  cd $PROJECT_DIR && source venv/bin/activate && python -m pytest -m local"
echo "Доменные:  sudo bash osnova_ad_join.sh   |   sudo bash osnova_freeipa_join.sh"