commit 8703b6615466dedfeba2814af5f6d92eac42b909 Author: a.gritsenko Date: Sun Sep 20 11:25:21 2026 +0300 Add CryptoPro license update and service restart scripts Co-Authored-By: Claude Opus 5 (1M context) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..72c1dd2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# логи выполнения +*.log + +# рабочие конфиги с реальными хостами/серийниками (в репо только *.example) +*.csv +*.сsv +!licenses.conf.example + +# сборка для переноса +pack/ diff --git a/license_update.sh b/license_update.sh new file mode 100644 index 0000000..1084950 --- /dev/null +++ b/license_update.sh @@ -0,0 +1,572 @@ +#!/bin/bash + +set -u +set -o pipefail + +LOG_FILE="./license_update.log" + +SSH_USER="" +CONFIG_FILE="" + +TOTAL=0 +SUCCESS=0 +ERRORS=0 + +log() { + local level="$1" + shift + echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $*" | tee -a "$LOG_FILE" +} + +usage() { + echo "Использование:" + echo " $0 -u SSH_USER -c CONFIG_FILE" + exit 1 +} + +while getopts "u:c:" opt; do + case "$opt" in + u) SSH_USER="$OPTARG" ;; + c) CONFIG_FILE="$OPTARG" ;; + *) usage ;; + esac +done + +[[ -z "$SSH_USER" ]] && usage +[[ -z "$CONFIG_FILE" ]] && usage + +if [[ ! -f "$CONFIG_FILE" ]]; then + echo "Ошибка: конфигурационный файл не найден: $CONFIG_FILE" + exit 1 +fi + +for cmd in ssh base64 mktemp date tee grep sed; do + if ! command -v "$cmd" >/dev/null 2>&1; then + echo "Ошибка: не найдена команда '$cmd'" >&2 + exit 1 + fi +done + +# ------------------------------------------------------------ +# Временные файлы (удалённый скрипт, вывод хоста) +# ------------------------------------------------------------ + +TMP_DIR="$(mktemp -d)" + +cleanup() { + rm -rf "$TMP_DIR" +} + +trap cleanup EXIT + +: > "$LOG_FILE" + +log INFO "==============================================" +log INFO "Запуск license_update.sh" +log INFO "SSH user: $SSH_USER" +log INFO "Config: $CONFIG_FILE" +log INFO "==============================================" + +# ------------------------------------------------------------ +# Формируем список хостов и операций. +# +# Формат: +# host;file;company;serial +# +# Для file=csp: +# host;csp;;serial +# ------------------------------------------------------------ + +declare -A HOST_OPERATIONS +declare -A HOST_OPS_COUNT +declare -A HOST_SEEN +HOST_ORDER=() + +while IFS=';' read -r host file company serial extra || [[ -n "$host" ]]; do + + # Убираем BOM из первой строки + host="${host#$'\xef\xbb\xbf'}" + + # Убираем CR от Windows-файлов + host="${host//$'\r'/}" + file="${file//$'\r'/}" + company="${company//$'\r'/}" + serial="${serial//$'\r'/}" + + # Пропускаем пустые строки + [[ -z "${host//[[:space:]]/}" ]] && continue + + # Пропускаем комментарии + [[ "$host" =~ ^[[:space:]]*# ]] && continue + + # Проверяем количество полей + if [[ -z "$file" || -z "$serial" ]]; then + log ERROR "Некорректная строка конфигурации: $host;$file;$company;$serial" + ((ERRORS++)) + ((TOTAL++)) + continue + fi + + # Для JSON-файла компания обязательна + if [[ "$file" != "csp" && -z "$company" ]]; then + log ERROR "Не указана компания для JSON-файла: $host;$file;$company;$serial" + ((ERRORS++)) + ((TOTAL++)) + continue + fi + + if [[ -z "${HOST_SEEN[$host]+x}" ]]; then + HOST_SEEN["$host"]=1 + HOST_ORDER+=("$host") + HOST_OPS_COUNT["$host"]=0 + fi + + # Используем разделитель TAB между полями одной операции + HOST_OPERATIONS["$host"]+="${file}"$'\t'"${company}"$'\t'"${serial}"$'\n' + HOST_OPS_COUNT["$host"]=$(( HOST_OPS_COUNT["$host"] + 1 )) + + ((TOTAL++)) + +done < "$CONFIG_FILE" + + +# ------------------------------------------------------------ +# SSH +# +# -tt обязательно: sudo на удалённом хосте запрашивает пароль +# через настоящий TTY. Пароль SSH спрашивает сам ssh. +# +# Локальный stdin остаётся терминалом и используется только +# для ввода паролей. Удалённый скрипт передаётся в командной +# строке (base64) и не занимает stdin. +# ------------------------------------------------------------ + +SSH_OPTS=( + -tt + -o ConnectTimeout=15 + -o ServerAliveInterval=30 + -o ServerAliveCountMax=3 +) + +# run_remote_script HOST LOCAL_SCRIPT +# Выполняет локальный файл скрипта на хосте под "sudo bash". +run_remote_script() { + local host="$1" + local script="$2" + local b64 + + b64=$(base64 -w0 "$script") + + ssh "${SSH_OPTS[@]}" "$SSH_USER@$host" \ + "t=\$(mktemp) && printf '%s' '$b64' | base64 -d > \"\$t\" && sudo bash \"\$t\"; rc=\$?; rm -f \"\$t\"; exit \$rc" +} + + +# ------------------------------------------------------------ +# Выполнение операций на одном хосте +# ------------------------------------------------------------ + +process_host() { + + local host="$1" + local operations="${HOST_OPERATIONS[$host]}" + local host_ops="${HOST_OPS_COUNT[$host]}" + + log INFO "----------------------------------------------" + log INFO "Host: $host" + + # -------------------------------------------------------- + # Генерируем удалённый скрипт. + # + # Скрипт выполняется на хосте под "sudo bash", поэтому + # внутри sudo не нужен. Список операций встраивается + # в конец скрипта как heredoc (stdin не используется). + # -------------------------------------------------------- + + local remote_script="$TMP_DIR/remote_$$.sh" + local host_out="$TMP_DIR/out_$$.log" + + cat > "$remote_script" <<'REMOTE_SCRIPT' +#!/bin/bash + +set -u + +if [[ "$EUID" -ne 0 ]]; then + echo "[REMOTE] ERROR: скрипт должен выполняться от root (sudo)" + exit 10 +fi + +echo "[REMOTE] sudo authentication successful" + +RESULT=0 + +op_result() { + echo "[REMOTE] OP_RESULT $1 $2" +} + +while IFS=$'\t' read -r FILE COMPANY SERIAL; do + + [[ -z "$FILE" ]] && continue + + echo + echo "[REMOTE] ==============================================" + echo "[REMOTE] File: $FILE" + echo "[REMOTE] SerialNumber: $SERIAL" + + # ======================================================== + # CryptoPro CSP + # ======================================================== + + if [[ "$FILE" == "csp" ]]; then + + echo "[REMOTE] Установка лицензии CryptoPro CSP" + + if /opt/cprocsp/sbin/amd64/cpconfig \ + -license \ + -set "$SERIAL" "$TMP" + then + + echo "[REMOTE] ERROR: Не удалось изменить JSON (блок License / SerialNumber / Company не найден)" + + rm -f "$TMP" + + # Восстанавливаем backup + cp -p "$BACKUP" "$FILE" + + RESULT=1 + op_result FAIL "$FILE" + continue + fi + + + # -------------------------------------------------------- + # Устанавливаем новый файл + # -------------------------------------------------------- + + if ! mv "$TMP" "$FILE"; then + echo "[REMOTE] ERROR: Не удалось заменить исходный файл" + + rm -f "$TMP" + cp -p "$BACKUP" "$FILE" + + RESULT=1 + op_result FAIL "$FILE" + continue + fi + + # -------------------------------------------------------- + # Восстанавливаем owner/mode + # -------------------------------------------------------- + + OP_OK=1 + + if ! chown "$OWNER" "$FILE"; then + echo "[REMOTE] ERROR: Не удалось восстановить owner" + RESULT=1 + OP_OK=0 + fi + + if ! chmod "$MODE" "$FILE"; then + echo "[REMOTE] ERROR: Не удалось восстановить mode" + RESULT=1 + OP_OK=0 + fi + + + # -------------------------------------------------------- + # Проверка результата + # -------------------------------------------------------- + + CHECK=$(awk ' + !in_license && /^[ ]*"License"[ ]*:[ ]*\{/ { + in_license = 1 + depth = 0 + } + + in_license && /^[ ]*"SerialNumber"[ ]*:/ { + print + } + + in_license && /^[ ]*"Company"[ ]*:/ { + print + } + + in_license { + opens = gsub(/\{/, "{") + closes = gsub(/\}/, "}") + + depth += opens + depth -= closes + + if (depth <= 0) + in_license = 0 + } + ' "$FILE") + + + if ! grep -Fq "\"SerialNumber\": \"$SERIAL_JSON\"" <<< "$CHECK"; then + echo "[REMOTE] ERROR: SerialNumber не прошёл проверку" + RESULT=1 + op_result FAIL "$FILE" + continue + fi + + if ! grep -Fq "\"Company\": \"$COMPANY_JSON\"" <<< "$CHECK"; then + echo "[REMOTE] ERROR: Company не прошёл проверку" + RESULT=1 + op_result FAIL "$FILE" + continue + fi + + if [[ "$OP_OK" -ne 1 ]]; then + op_result FAIL "$FILE" + continue + fi + + echo "[REMOTE] JSON успешно обновлён" + op_result OK "$FILE" + +done <<'OPERATIONS' +REMOTE_SCRIPT + + # Добавляем операции (heredoc) и завершаем удалённый скрипт + printf '%s' "$operations" >> "$remote_script" + printf 'OPERATIONS\n\nexit $RESULT\n' >> "$remote_script" + + # -------------------------------------------------------- + # Выполнение. + # + # Вывод удалённого скрипта идёт через tee одновременно + # в лог и во временный файл (для подсчёта результатов). + # tee не буферизует вывод, поэтому приглашение sudo + # (без перевода строки) видно сразу. + # -------------------------------------------------------- + + log INFO "[$host] Подключение по SSH..." + + : > "$host_out" + + local rc=0 + run_remote_script "$host" "$remote_script" 2>&1 | tee -a "$LOG_FILE" "$host_out" || rc=$? + + # pty на удалённой стороне добавляет CR + sed -i 's/\r$//' "$host_out" + + local ok + ok=$(grep -c '^\[REMOTE\] OP_RESULT OK ' "$host_out") + ok=${ok:-0} + + local failed=$(( host_ops - ok )) + + SUCCESS=$(( SUCCESS + ok )) + ERRORS=$(( ERRORS + failed )) + + if [[ "$rc" -eq 0 && "$failed" -eq 0 ]]; then + log INFO "[$host] Операции выполнены успешно ($ok из $host_ops)" + elif [[ "$rc" -eq 255 ]]; then + log ERROR "[$host] Ошибка SSH-соединения (операций с ошибкой: $failed из $host_ops)" + else + log ERROR "[$host] Ошибка выполнения операций (rc=$rc, успешно: $ok из $host_ops)" + fi + + rm -f "$remote_script" "$host_out" +} + + +# ------------------------------------------------------------ +# Обработка всех хостов +# ------------------------------------------------------------ + +for host in ${HOST_ORDER[@]+"${HOST_ORDER[@]}"}; do + process_host "$host" +done + + +# ------------------------------------------------------------ +# Итог +# ------------------------------------------------------------ + +log INFO "==============================================" +log INFO "Результат выполнения" +log INFO "Всего операций : $TOTAL" +log INFO "Успешно : $SUCCESS" +log INFO "Ошибок : $ERRORS" +log INFO "Log : $LOG_FILE" +log INFO "==============================================" + +# Убираем CR, попавшие в лог из удалённого TTY +sed -i 's/\r$//' "$LOG_FILE" + +if (( ERRORS > 0 )); then + exit 1 +fi + +exit 0 diff --git a/licenses.conf.example b/licenses.conf.example new file mode 100644 index 0000000..8d8ffec --- /dev/null +++ b/licenses.conf.example @@ -0,0 +1,6 @@ +# host;file;company;serial_number + +10.10.10.11;/opt/app/config.json;ООО Ромашка и Ко;R123H-12345-12LG4-WR6NU-1234W +10.10.10.12;/etc/myapp/config.json;АО Научно Производственный Центр;A111B-22222-33CC4-DD5EE-6666F +10.10.10.13;/opt/license/config.json;Загуглинск;R555X-55555-55ZZ5-AA5BB-5555Z +10.10.10.13;csp;;50500-03010-03MCZ-921DC-HGLK4 diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..99a7cb3 --- /dev/null +++ b/readme.md @@ -0,0 +1,744 @@ +# Скрипты управления лицензиями и службами CryptoPro + +## 1. Назначение + +Набор Bash-скриптов предназначен для централизованного выполнения операций на удалённых Linux/Astra Linux-серверах: + +* установки лицензий CryptoPro CSP; +* изменения параметров лицензии в JSON-конфигурациях приложений; +* перезапуска служб после изменения лицензии; +* проверки успешности выполненных операций; +* создания резервных копий изменяемых конфигурационных файлов; +* ведения локального журнала выполнения. + +Работа выполняется по SSH под указанной учётной записью. + +Для выполнения административных операций используется `sudo`. + +Пароли не хранятся в конфигурационных файлах, не передаются в параметрах командной строки и не записываются в журнал. + +--- + +# 2. Формат конфигурационного файла + +Основной конфигурационный файл использует формат: + +```text +host;file;company;serial_number +``` + +Пример: + +```text +# host;file;company;serial_number + +10.11.1.11;csp;;50500-03010-03MCZ-921DC-HGLK4 + +10.11.1.12;/etc/cprocsp/app/appsettings.json;ООО Ромашка;50500-03010-03MCZ-921DC-HGLK5 +``` + +Поля: + +| Поле | Описание | +| --------------- | ------------------------------------------------- | +| `host` | IP-адрес или DNS-имя удалённого сервера | +| `file` | Специальное значение `csp` либо путь к JSON-файлу | +| `company` | Наименование организации для JSON-конфигурации | +| `serial_number` | Серийный номер лицензии | + +Разделителем является символ `;`. + +Для строк с JSON-файлом поле `company` обязательно; для `csp` оно игнорируется. +Файл может быть с окончаниями строк CRLF (Windows) и BOM. + +Компания может содержать пробелы: + +```text +10.11.1.12;/etc/app/appsettings.json;ООО Тестовая Организация;50500-03010-03MCZ-921DC-HGLK5 +``` + +Символ `;` внутри значения компании использовать нельзя, поскольку он является разделителем полей. + +--- + +# 3. Комментарии и пустые строки + +В конфигурационном файле разрешены пустые строки. + +Также разрешены комментарии, начинающиеся с `#`: + +```text +# Серверы УЦ + +10.11.1.11;csp;;50500-03010-03MCZ-921DC-HGLK4 + +### Сервер приложений + +10.11.1.12;/etc/app/appsettings.json;ООО Ромашка;50500-03010-03MCZ-HGLK5 +``` + +Комментарии и пустые строки не обрабатываются. + +--- + +# 4. Скрипт `license_update.sh` + +## 4.1. Назначение + +`license_update.sh` выполняет установку или изменение лицензии на удалённых серверах. + +Запуск: + +```bash +./license_update.sh -u administrator -c ./licenses_local_test.csv +``` + +где: + +* `-u` — SSH-пользователь; +* `-c` — путь к конфигурационному файлу. + +Например: + +```bash +./license_update.sh \ + -u administrator \ + -c ./licenses_local_test.csv +``` + +--- + +# 5. Подключение к серверу + +Для каждого сервера скрипт устанавливает SSH-соединение: + +```text +administrator@10.11.1.11's password: +``` + +Пароль SSH вводится интерактивно. + +Пароль не передаётся в командной строке: + +```text +НЕ используется: +sshpass +-p password +``` + +и не записывается в лог. + +--- + +# 6. Авторизация sudo + +После успешного подключения сформированный скрипт операций передаётся на сервер +(в командной строке ssh, в кодировке base64, без использования stdin) и запускается: + +```bash +sudo bash /tmp/tmp.XXXXXX +``` + +Пароль `sudo` запрашивается непосредственно на удалённом сервере, один раз на хост: + +```text +[sudo] password for administrator: +``` + +Для этого SSH-соединение создаётся с TTY (`ssh -tt`), а локальный stdin остаётся +терминалом и используется только для ввода паролей SSH и `sudo`. + +После успешной авторизации выполняются все административные операции текущего сеанса. + +При первом подключении к серверу ssh стандартно спросит подтверждение ключа хоста +(`Are you sure you want to continue connecting (yes/no)?`). + +Пароль `sudo`: + +* не передаётся через stdin; +* не хранится в переменной Bash; +* не записывается в лог; +* не находится в конфигурационном файле. + +--- + +# 7. Обработка `file=csp` + +Если в конфигурации указано: + +```text +10.11.1.11;csp;;50500-03010-03MCZ-921DC-HGLK4 +``` + +скрипт не изменяет JSON-файлы. + +Выполняется: + +```bash +/opt/cprocsp/sbin/amd64/cpconfig \ + -license \ + -set "50500-03010-03MCZ-921DC-HGLK4" +``` + +Команда выполняется с правами `sudo`. + +При успешной установке лицензии выполняется перезапуск службы: + +```bash +systemctl restart cprocsp +``` + +После перезапуска проверяется состояние службы: + +```bash +systemctl is-active --quiet cprocsp +``` + +Ожидаемый результат: + +```text +cprocsp is active +``` + +Если служба не перешла в состояние `active`, в журнал дополнительно выводится: + +```bash +systemctl status cprocsp --no-pager --full +``` + +Операция считается ошибочной. + +--- + +# 8. Обработка JSON-файлов + +Если вместо `csp` указан путь к JSON: + +```text +10.11.1.12;/etc/app/appsettings.json;ООО Ромашка;50500-03010-03MCZ-HGLK5 +``` + +скрипт выполняет следующие операции. + +## 8.1. Проверка файла + +Проверяется существование файла: + +```bash +test -f "$FILE" +``` + +Если файл отсутствует, операция завершается ошибкой. + +--- + +## 8.2. Сохранение атрибутов + +До изменения файла сохраняются: + +* UID владельца; +* GID владельца; +* права доступа. + +Например: + +```text +Owner: 1000:1000 +Mode : 640 +``` + +--- + +## 8.3. Создание резервной копии + +Перед изменением создаётся backup: + +```text +/etc/app/appsettings.json.license_backup_20260918_123456_12345 +``` + +Резервная копия создаётся с сохранением исходных атрибутов: + +```bash +cp -p +``` + +Путь к backup записывается в журнал. + +--- + +# 9. Изменение JSON + +Скрипт изменяет только два параметра внутри объекта: + +```json +"License": { + "SerialNumber": "...", + "Company": "..." +} +``` + +Изменяются: + +```text +License.SerialNumber +License.Company +``` + +Например, было: + +```json +"License": { + "SerialNumber": "OLD-LICENSE", + "Company": "Старая организация" +} +``` + +становится: + +```json +"License": { + "SerialNumber": "50500-03010-03MCZ-921DC-HGLK5", + "Company": "ООО Ромашка" +} +``` + +Остальные параметры JSON не должны изменяться скриптом. + +--- + +# 10. Временный файл + +Изменение выполняется через временный файл: + +```text +appsettings.json.tmp. +``` + +Сначала формируется новая версия файла, после чего она заменяет исходный. + +Это позволяет не изменять исходный файл непосредственно во время обработки. + +--- + +# 11. Восстановление атрибутов + +После замены файла восстанавливаются исходные: + +```text +owner +permissions +``` + +Используются: + +```bash +chown +chmod +``` + +Таким образом, изменение содержимого файла не должно приводить к изменению его владельца или прав доступа. + +--- + +# 12. Проверка результата + +После изменения выполняется повторная проверка параметров: + +```text +License.SerialNumber +License.Company +``` + +Проверяется наличие установленных значений. + +Если проверка не пройдена, операция считается ошибочной. + +При ошибке изменения JSON скрипт пытается восстановить исходный файл из backup. + +Итог по каждой операции выводится строкой: + +```text +[REMOTE] OP_RESULT OK /opt/app/appsettings.json +[REMOTE] OP_RESULT FAIL csp +``` + +По этим строкам формируется итоговая статистика (успешно / ошибок). + +--- + +# 13. Обработка нескольких записей одного сервера + +Если один сервер встречается в конфигурации несколько раз: + +```text +10.11.1.11;csp;;LICENSE-001 + +10.11.1.11;/etc/app/appsettings.json;ООО Ромашка;LICENSE-002 +``` + +операции группируются по хосту. + +Для одного хоста используется один SSH-сеанс. + +После подключения выполняется авторизация `sudo`, затем последовательно обрабатываются все операции этого сервера. + +Это позволяет не выполнять повторное подключение для каждой строки конфигурации. + +--- + +# 14. Журнал работы + +Основной журнал: + +```text +license_update.log +``` + +Пример: + +```text +[2026-09-18 09:23:08] [INFO] Запуск license_update.sh +[2026-09-18 09:23:08] [INFO] SSH user: administrator +[2026-09-18 09:23:08] [INFO] Config: ./licenses_local_test.csv + +[2026-09-18 09:23:08] [INFO] Host: 10.11.1.11 +``` + +В журнале фиксируются: + +* начало выполнения; +* используемый SSH-пользователь; +* конфигурационный файл; +* обрабатываемые серверы; +* результаты операций; +* ошибки; +* путь к backup; +* итоговая статистика. + +Пароли в журнал не записываются. + +--- + +# 15. Итоговая статистика + +В конце выполнения выводится: + +```text +============================================== +Результат выполнения +Всего операций : 10 +Успешно : 9 +Ошибок : 1 +Log : ./license_update.log +============================================== +``` + +Если хотя бы одна операция завершилась ошибкой, скрипт возвращает код: + +```text +1 +``` + +При полном успешном выполнении: + +```text +0 +``` + +Это позволяет использовать скрипт в других Bash-скриптах или системах автоматизации. + +--- + +# 16. Скрипт `restart_services.sh` + +`restart_services.sh` предназначен для централизованного перезапуска служб на серверах. + +Формат конфигурации: + +```text +host;services +``` + +Пример: + +```text +# host;services + +ip_ca_tech;cp.ca-tech +ip_ca_tls;cp.ca-tls +ip_ca_digrub;cp.ca-digrub + +ip_ra_tech;cp.ra-tech,cp.ra-web-tech +ip_ra_tls;cp.ra-tls,cp.ra-web-tls,cp.cr-tls,cp.cr-web.tls +ip_ra_digrub;cp.ra-digrub,cp.ra-web-digrub,cp.cr-digrub,cp.cr-web.digrub +``` + +--- + +# 17. Запуск `restart_services.sh` + +Пример: + +```bash +./restart_services.sh \ + -u administrator \ + -c ./services.csv +``` + +Подключение и авторизация `sudo` выполняются так же, как в `license_update.sh` +(см. разделы 5 и 6): пароль SSH спрашивает ssh, пароль `sudo` запрашивается +на удалённом сервере через TTY, один раз на хост. Пароль в переменных скрипта не хранится. + +Затем службы перезапускаются последовательно. + +--- + +# 18. Перезапуск службы + +Для каждой службы выполняется: + +```bash +systemctl restart SERVICE +``` + +Например: + +```bash +systemctl restart cp.ca-tech +``` + +После перезапуска выполняется небольшая пауза и проверяется состояние: + +```bash +systemctl is-active --quiet cp.ca-tech +``` + +Если служба успешно запущена: + +```text +cp.ca-tech is active +``` + +Если запуск не удался, выводится подробная информация: + +```bash +systemctl status cp.ca-tech --no-pager --full +``` + +--- + +# 19. Несколько служб на одном сервере + +Службы разделяются запятыми: + +```text +ip_ra_tech;cp.ra-tech,cp.ra-web-tech +``` + +Скрипт преобразует эту запись в две операции: + +```bash +systemctl restart cp.ra-tech +systemctl restart cp.ra-web-tech +``` + +Службы перезапускаются последовательно. + +--- + +# 20. Журнал `restart_services.sh` + +Для отдельного скрипта используется собственный журнал: + +```text +restart_services.log +``` + +В журнале фиксируются: + +* сервер; +* служба; +* результат `systemctl restart`; +* состояние службы после перезапуска; +* диагностический вывод при ошибке; +* итоговая статистика. + +--- + +# 21. Общая схема работы + +Общая последовательность для удалённого сервера: + +```text + ┌──────────────────────┐ + │ Конфигурационный │ + │ файл │ + └──────────┬───────────┘ + │ + ▼ + ┌──────────────────────┐ + │ Группировка по host │ + └──────────┬───────────┘ + │ + ▼ + ┌──────────────────────┐ + │ SSH connection │ + └──────────┬───────────┘ + │ + ▼ + ┌──────────────────────┐ + │ sudo -v │ + │ интерактивный пароль │ + └──────────┬───────────┘ + │ + ┌─────────┴─────────┐ + │ │ + ▼ ▼ + file = csp JSON-файл + │ │ + ▼ ▼ + cpconfig -license backup + │ │ + ▼ ▼ + restart cprocsp изменение + │ │ + ▼ ▼ + проверка active проверка + │ + ▼ + восстановление + owner/mode + │ + ┌───────────────────┘ + ▼ + Результат + │ + ▼ + Локальный log +``` + +--- + +# 22. Требования + +Для работы скриптов необходимы: + +### На управляющей машине + +* Bash; +* OpenSSH client; +* стандартные Unix-утилиты (coreutils): + + * `base64`; + * `grep`; + * `sed`; + * `mktemp`; + * `date`; + * `tee`. + +### На удалённых серверах + +* SSH-сервер; +* `sudo`; +* `systemd`; +* `bash`, `awk` (gawk или mawk), `base64`, `mktemp`; +* для установки CSP-лицензии: + `/opt/cprocsp/sbin/amd64/cpconfig`; +* для JSON-конфигураций: + доступ к указанному файлу. + +Пользователь SSH должен иметь возможность выполнять соответствующие команды через `sudo`. + +--- + +# 23. Безопасность + +Скрипты не должны содержать пароли в конфигурации. + +Не рекомендуется использовать: + +```bash +./license_update.sh -u administrator -p password +``` + +Пароль SSH вводится стандартным механизмом OpenSSH. + +Пароль `sudo` вводится стандартным механизмом `sudo`. + +Пароли не должны попадать: + +* в командную строку; +* в конфигурационные файлы; +* в переменные скрипта; +* в `license_update.log`; +* в `restart_services.log`. + +--- + +# 24. Рекомендации по эксплуатации + +Перед массовым запуском рекомендуется: + +1. Проверить подключение по SSH: + +```bash +ssh administrator@10.11.1.11 +``` + +2. Проверить возможность выполнения `sudo`: + +```bash +ssh -tt administrator@10.11.1.11 'sudo -v' +``` + +3. Проверить наличие CryptoPro: + +```bash +ssh -tt administrator@10.11.1.11 \ + 'sudo /opt/cprocsp/sbin/amd64/cpconfig -license -view' +``` + +4. Проверить конфигурационный файл. + +5. Сначала выполнить скрипт на одном тестовом сервере. + +6. Проверить backup и журнал. + +7. После успешной проверки выполнить обработку остальных серверов. + +--- + +# 25. Коды завершения + +| Код | Значение | +| --: | ----------------------------------------------- | +| `0` | Все операции выполнены успешно | +| `1` | Одна или несколько операций завершились ошибкой | + +Пример использования в другом скрипте: + +```bash +./license_update.sh -u administrator -c licenses.csv + +if [[ $? -eq 0 ]]; then + echo "Лицензии успешно обновлены" +else + echo "При обновлении лицензий произошли ошибки" +fi +``` + +--- + +# 26. Результат + +В результате работы: + +* лицензии CryptoPro CSP устанавливаются централизованно; +* JSON-конфигурации изменяются только в части параметров `License.SerialNumber` и `License.Company`; +* перед изменением JSON создаётся резервная копия; +* исходные права и владелец файла сохраняются; +* службы после необходимых изменений проверяются; +* операции выполняются с административными правами через `sudo`; +* пароли не сохраняются и не попадают в журналы; +* все результаты доступны в локальных log-файлах. diff --git a/restart_services.sh b/restart_services.sh new file mode 100644 index 0000000..d9077e0 --- /dev/null +++ b/restart_services.sh @@ -0,0 +1,366 @@ +#!/usr/bin/env bash + +set -u +set -o pipefail + +SCRIPT_NAME="$(basename "$0")" +LOG_FILE="./restart_services.log" + +SSH_USER="" +CONFIG_FILE="" + +# ---------------------------------------------------------------------- +# Logging +# ---------------------------------------------------------------------- + +log() { + local level="$1" + shift + + printf '[%s] [%s] %s\n' \ + "$(date '+%Y-%m-%d %H:%M:%S')" \ + "$level" \ + "$*" | tee -a "$LOG_FILE" +} + +# ---------------------------------------------------------------------- +# Usage +# ---------------------------------------------------------------------- + +usage() { + cat <&2 + usage + exit 1 + ;; + :) + echo "Для параметра -$OPTARG требуется значение." >&2 + usage + exit 1 + ;; + esac +done + +if [[ -z "$SSH_USER" || -z "$CONFIG_FILE" ]]; then + usage + exit 1 +fi + +if [[ ! -f "$CONFIG_FILE" ]]; then + echo "Ошибка: файл конфигурации не найден: $CONFIG_FILE" >&2 + exit 1 +fi + +# ---------------------------------------------------------------------- +# Dependencies +# ---------------------------------------------------------------------- + +for cmd in ssh base64 mktemp date tee grep sed; do + if ! command -v "$cmd" >/dev/null 2>&1; then + echo "Ошибка: не найдена команда '$cmd'" >&2 + exit 1 + fi +done + +# ---------------------------------------------------------------------- +# Temp files / cleanup +# ---------------------------------------------------------------------- + +TMP_DIR="$(mktemp -d)" + +cleanup() { + rm -rf "$TMP_DIR" +} + +trap cleanup EXIT + +# ---------------------------------------------------------------------- +# SSH +# +# -tt обязательно: sudo на удалённом хосте запрашивает пароль +# через настоящий TTY. Пароль SSH спрашивает сам ssh. +# +# Локальный stdin остаётся терминалом и используется только +# для ввода паролей. Удалённый скрипт передаётся в командной +# строке (base64) и не занимает stdin. +# ---------------------------------------------------------------------- + +SSH_OPTS=( + -tt + -o ConnectTimeout=15 + -o ServerAliveInterval=30 + -o ServerAliveCountMax=3 +) + +# run_remote_script HOST LOCAL_SCRIPT +# Выполняет локальный файл скрипта на хосте под "sudo bash". +run_remote_script() { + local host="$1" + local script="$2" + local b64 + + b64=$(base64 -w0 "$script") + + ssh "${SSH_OPTS[@]}" "$SSH_USER@$host" \ + "t=\$(mktemp) && printf '%s' '$b64' | base64 -d > \"\$t\" && sudo bash \"\$t\"; rc=\$?; rm -f \"\$t\"; exit \$rc" +} + +# ---------------------------------------------------------------------- +# Helpers +# ---------------------------------------------------------------------- + +trim() { + local value="$1" + + value="${value#"${value%%[![:space:]]*}"}" + value="${value%"${value##*[![:space:]]}"}" + + printf '%s' "$value" +} + +# ---------------------------------------------------------------------- +# Restart services +# ---------------------------------------------------------------------- + +restart_services() { + local host="$1" + local services="$2" + + log INFO "[$host] Подготовка к перезапуску служб" + + local remote_script="$TMP_DIR/remote_$$.sh" + local host_out="$TMP_DIR/out_$$.log" + + # Список служб встраивается в скрипт (heredoc), stdin не используется + { + cat <<'REMOTE_SCRIPT' +#!/bin/bash + +set -u +set -o pipefail + +if [[ "$EUID" -ne 0 ]]; then + echo "ERROR: скрипт должен выполняться от root (sudo)" + exit 10 +fi + +SERVICES=$(cat <<'SERVICES_LIST' +REMOTE_SCRIPT + + printf '%s\n' "$services" + + cat <<'REMOTE_SCRIPT' +SERVICES_LIST +) + +FAILED=0 + +IFS="," read -ra SERVICE_LIST <<< "$SERVICES" + +for SERVICE in "${SERVICE_LIST[@]}"; do + + # Убираем пробелы + SERVICE="${SERVICE#"${SERVICE%%[![:space:]]*}"}" + SERVICE="${SERVICE%"${SERVICE##*[![:space:]]}"}" + + if [[ -z "$SERVICE" ]]; then + continue + fi + + echo "--------------------------------------------------" + echo "Service: $SERVICE" + + # Проверяем наличие unit + if ! systemctl cat "$SERVICE" >/dev/null 2>&1 &1 || true + + FAILED=1 + fi + +done + +echo "--------------------------------------------------" + +if [[ "$FAILED" -ne 0 ]]; then + echo "RESULT=FAILED" + exit 1 +fi + +echo "RESULT=SUCCESS" +exit 0 +REMOTE_SCRIPT + } > "$remote_script" + + # ------------------------------------------------------------------ + # Выполнение. + # + # Вывод удалённого скрипта идёт через tee одновременно в лог + # и во временный файл (для проверки результата). tee не буферизует + # вывод, поэтому приглашение sudo (без перевода строки) видно сразу. + # ------------------------------------------------------------------ + + : > "$host_out" + + local rc=0 + run_remote_script "$host" "$remote_script" 2>&1 | tee -a "$LOG_FILE" "$host_out" || rc=$? + + # pty на удалённой стороне добавляет CR + sed -i 's/\r$//' "$host_out" + + rm -f "$remote_script" + + if [[ "$rc" -eq 255 ]]; then + log ERROR "[$host] Ошибка SSH-соединения" + return 1 + fi + + if [[ "$rc" -eq 0 ]] && grep -q '^RESULT=SUCCESS$' "$host_out"; then + log INFO "[$host] Все службы успешно перезапущены" + return 0 + fi + + log ERROR "[$host] Не все службы были успешно перезапущены (rc=$rc)" + return 1 +} + +# ---------------------------------------------------------------------- +# Main +# ---------------------------------------------------------------------- + +log INFO "==============================================" +log INFO "Запуск $SCRIPT_NAME" +log INFO "SSH user: $SSH_USER" +log INFO "Config: $CONFIG_FILE" +log INFO "==============================================" + +TOTAL=0 +SUCCESS=0 +FAILED=0 + +# ---------------------------------------------------------------------- +# Читаем конфигурацию в массивы заранее: во время ssh stdin должен +# оставаться терминалом (ввод паролей), а не файлом конфигурации. +# ---------------------------------------------------------------------- + +HOSTS=() +HOST_SERVICES=() + +while IFS=';' read -r host services || [[ -n "$host" ]]; do + + # Убираем BOM из первой строки + host="${host#$'\xef\xbb\xbf'}" + + host="$(trim "$host")" + services="$(trim "$services")" + + # Пустая строка + [[ -z "$host" ]] && continue + + # Комментарий + [[ "$host" == \#* ]] && continue + + # Проверяем конфигурацию + if [[ -z "$services" ]]; then + log ERROR "[$host] Не указан список служб" + ((FAILED++)) + ((TOTAL++)) + continue + fi + + HOSTS+=("$host") + HOST_SERVICES+=("$services") + ((TOTAL++)) + +done < "$CONFIG_FILE" + +for i in ${HOSTS[@]+"${!HOSTS[@]}"}; do + host="${HOSTS[$i]}" + services="${HOST_SERVICES[$i]}" + + log INFO "----------------------------------------------" + log INFO "Host: $host" + log INFO "Services: $services" + + if restart_services "$host" "$services"; then + ((SUCCESS++)) + else + ((FAILED++)) + fi +done + +# ---------------------------------------------------------------------- +# Summary +# ---------------------------------------------------------------------- + +log INFO "==============================================" +log INFO "Результат выполнения" +log INFO "Всего хостов : $TOTAL" +log INFO "Успешно : $SUCCESS" +log INFO "Ошибок : $FAILED" +log INFO "Log : $LOG_FILE" +log INFO "==============================================" + +# Убираем CR, попавшие в лог из удалённого TTY +sed -i 's/\r$//' "$LOG_FILE" + +if [[ $FAILED -gt 0 ]]; then + exit 1 +fi + +exit 0