Add CryptoPro license update and service restart scripts
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+10
@@ -0,0 +1,10 @@
|
|||||||
|
# логи выполнения
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# рабочие конфиги с реальными хостами/серийниками (в репо только *.example)
|
||||||
|
*.csv
|
||||||
|
*.сsv
|
||||||
|
!licenses.conf.example
|
||||||
|
|
||||||
|
# сборка для переноса
|
||||||
|
pack/
|
||||||
@@ -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" </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
|
||||||
@@ -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
|
||||||
@@ -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.<PID>
|
||||||
|
```
|
||||||
|
|
||||||
|
Сначала формируется новая версия файла, после чего она заменяет исходный.
|
||||||
|
|
||||||
|
Это позволяет не изменять исходный файл непосредственно во время обработки.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 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-файлах.
|
||||||
@@ -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 <<EOF
|
||||||
|
Использование:
|
||||||
|
|
||||||
|
$SCRIPT_NAME -u USER -c CONFIG
|
||||||
|
|
||||||
|
Параметры:
|
||||||
|
|
||||||
|
-u USER SSH пользователь
|
||||||
|
-c CONFIG файл конфигурации (host;service1,service2,...)
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
|
||||||
|
$SCRIPT_NAME -u admin -c ./services.csv
|
||||||
|
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
# Arguments
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
while getopts ":u:c:h" opt; do
|
||||||
|
case "$opt" in
|
||||||
|
u)
|
||||||
|
SSH_USER="$OPTARG"
|
||||||
|
;;
|
||||||
|
c)
|
||||||
|
CONFIG_FILE="$OPTARG"
|
||||||
|
;;
|
||||||
|
h)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
\?)
|
||||||
|
echo "Неизвестный параметр: -$OPTARG" >&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 </dev/null; then
|
||||||
|
echo "ERROR: systemd unit not found: $SERVICE"
|
||||||
|
FAILED=1
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Restarting $SERVICE..."
|
||||||
|
|
||||||
|
if ! systemctl restart "$SERVICE" </dev/null; then
|
||||||
|
echo "ERROR: failed to restart $SERVICE"
|
||||||
|
FAILED=1
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Небольшая пауза перед проверкой
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
if systemctl is-active --quiet "$SERVICE"; then
|
||||||
|
echo "SUCCESS: $SERVICE is active"
|
||||||
|
else
|
||||||
|
echo "ERROR: $SERVICE is not active after restart"
|
||||||
|
|
||||||
|
systemctl status "$SERVICE" \
|
||||||
|
--no-pager \
|
||||||
|
--full </dev/null 2>&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
|
||||||
Reference in New Issue
Block a user