Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
573 lines
16 KiB
Bash
573 lines
16 KiB
Bash
#!/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" </dev/null
|
|
then
|
|
echo "[REMOTE] CSP license installed successfully"
|
|
else
|
|
echo "[REMOTE] ERROR: Ошибка установки CSP лицензии"
|
|
RESULT=1
|
|
op_result FAIL "$FILE"
|
|
continue
|
|
fi
|
|
|
|
echo "[REMOTE] Перезапуск cprocsp"
|
|
|
|
if systemctl restart cprocsp </dev/null; then
|
|
sleep 2
|
|
|
|
if systemctl is-active --quiet cprocsp; then
|
|
echo "[REMOTE] cprocsp is active"
|
|
op_result OK "$FILE"
|
|
else
|
|
echo "[REMOTE] ERROR: cprocsp is not active"
|
|
|
|
systemctl status cprocsp \
|
|
--no-pager \
|
|
--full </dev/null || true
|
|
|
|
RESULT=1
|
|
op_result FAIL "$FILE"
|
|
fi
|
|
else
|
|
echo "[REMOTE] ERROR: Не удалось перезапустить cprocsp"
|
|
RESULT=1
|
|
op_result FAIL "$FILE"
|
|
fi
|
|
|
|
continue
|
|
fi
|
|
|
|
|
|
# ========================================================
|
|
# JSON configuration
|
|
# ========================================================
|
|
|
|
if [[ ! -f "$FILE" ]]; then
|
|
echo "[REMOTE] ERROR: Файл не найден: $FILE"
|
|
RESULT=1
|
|
op_result FAIL "$FILE"
|
|
continue
|
|
fi
|
|
|
|
echo "[REMOTE] Обновление JSON"
|
|
|
|
# --------------------------------------------------------
|
|
# Сохраняем owner и mode исходного файла
|
|
# --------------------------------------------------------
|
|
|
|
OWNER=$(stat -c '%u:%g' "$FILE")
|
|
MODE=$(stat -c '%a' "$FILE")
|
|
|
|
echo "[REMOTE] Owner: $OWNER"
|
|
echo "[REMOTE] Mode : $MODE"
|
|
|
|
# --------------------------------------------------------
|
|
# Backup
|
|
# --------------------------------------------------------
|
|
|
|
BACKUP="${FILE}.license_backup_$(date '+%Y%m%d_%H%M%S')_$$"
|
|
|
|
if ! cp -p "$FILE" "$BACKUP"; then
|
|
echo "[REMOTE] ERROR: Не удалось создать backup"
|
|
RESULT=1
|
|
op_result FAIL "$FILE"
|
|
continue
|
|
fi
|
|
|
|
echo "[REMOTE] Backup: $BACKUP"
|
|
|
|
TMP="${FILE}.tmp.$$"
|
|
|
|
# --------------------------------------------------------
|
|
# Изменяем только:
|
|
#
|
|
# License.SerialNumber
|
|
# License.Company
|
|
#
|
|
# Остальной JSON не трогаем.
|
|
# --------------------------------------------------------
|
|
|
|
# Экранируем значения для JSON-строки (\ и ").
|
|
# Передаём через окружение: awk -v обрабатывает backslash-последовательности.
|
|
COMPANY_JSON="${COMPANY//\\/\\\\}"
|
|
COMPANY_JSON="${COMPANY_JSON//\"/\\\"}"
|
|
SERIAL_JSON="${SERIAL//\\/\\\\}"
|
|
SERIAL_JSON="${SERIAL_JSON//\"/\\\"}"
|
|
|
|
if ! company="$COMPANY_JSON" serial="$SERIAL_JSON" awk '
|
|
BEGIN {
|
|
in_license = 0
|
|
depth = 0
|
|
serial_found = 0
|
|
company_found = 0
|
|
|
|
company = ENVIRON["company"]
|
|
serial = ENVIRON["serial"]
|
|
}
|
|
|
|
{
|
|
line = $0
|
|
|
|
# Строка "License": { сама содержит открывающую скобку,
|
|
# она будет учтена счётчиком ниже, поэтому depth = 0.
|
|
if (!in_license &&
|
|
line ~ /^[ ]*"License"[ ]*:[ ]*\{/) {
|
|
|
|
in_license = 1
|
|
depth = 0
|
|
}
|
|
|
|
if (in_license &&
|
|
line ~ /^[ ]*"SerialNumber"[ ]*:/) {
|
|
|
|
indent = ""
|
|
if (match(line, /^[ ]*/))
|
|
indent = substr(line, RSTART, RLENGTH)
|
|
|
|
comma = ""
|
|
if (line ~ /,[ ]*$/)
|
|
comma = ","
|
|
|
|
line = indent "\"SerialNumber\": \"" serial "\"" comma
|
|
serial_found = 1
|
|
}
|
|
|
|
if (in_license &&
|
|
line ~ /^[ ]*"Company"[ ]*:/) {
|
|
|
|
indent = ""
|
|
if (match(line, /^[ ]*/))
|
|
indent = substr(line, RSTART, RLENGTH)
|
|
|
|
comma = ""
|
|
if (line ~ /,[ ]*$/)
|
|
comma = ","
|
|
|
|
line = indent "\"Company\": \"" company "\"" comma
|
|
company_found = 1
|
|
}
|
|
|
|
print line
|
|
|
|
if (in_license) {
|
|
opens = gsub(/\{/, "{", line)
|
|
closes = gsub(/\}/, "}", line)
|
|
|
|
depth += opens
|
|
depth -= closes
|
|
|
|
if (depth <= 0)
|
|
in_license = 0
|
|
}
|
|
}
|
|
|
|
END {
|
|
if (!serial_found)
|
|
exit 20
|
|
|
|
if (!company_found)
|
|
exit 21
|
|
}
|
|
' "$FILE" > "$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
|