#!/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