Есть один любопытный закон развития инфраструктуры. Сначала инженер управляет виртуальными машинами через веб-интерфейс. Затем открывает для себя Bash и автоматизирует всё подряд: резервные копии, обновления, мониторинг и даже развёртывание сервисов. Потом появляются Terraform, Ansible, GitOps и Kubernetes, после чего кто-нибудь обязательно заявляет: "Bash умер".

Проходит несколько лет, и выясняется, что очередной инцидент снова приходится устранять небольшим сценарием оболочки, написанным за десять минут.

Именно поэтому Bash до сих пор жив. Да, он больше не является основным инструментом управления крупной инфраструктурой, но остаётся великолепным швейцарским ножом инженера. Особенно когда необходимо быстро решить конкретную задачу без развёртывания ещё одного уровня автоматизации.

Одной из таких задач является корректная перезагрузка виртуальных машин в Proxmox VE.

Почему простая перезагрузка - не всегда хорошая идея

Многие администраторы используют:

qm reboot 101

или вовсе выполняют принудительный сброс питания.

Проблема заключается в том, что гостевая операционная система может не успеть завершить работу приложений и записать данные на диск. Особенно болезненно подобные действия переживают базы данных, файловые серверы и различные корпоративные приложения.

Гораздо безопаснее действовать по следующему сценарию:

  • определить, на каком узле кластера находится виртуальная машина;

  • отправить ей команду штатного завершения работы;

  • дождаться полной остановки;

  • убедиться, что машина действительно выключилась;

  • выполнить запуск;

  • проверить успешность старта.

Именно такую логику мы реализуем.

Bash-скрипт для безопасной перезагрузки

В исходном варианте сценарий был вполне рабочим, однако содержал несколько потенциально опасных мест. Например, он мог ожидать завершения работы бесконечно.

Ниже приведён доработанный вариант.

#!/bin/bash

set -euo pipefail

TIMEOUT=600
POLL_INTERVAL=5

if ! command -v jq >/dev/null 2>&1; then
    echo "Ошибка: пакет jq не установлен"
    echo "Установите его командой:"
    echo "apt install jq"
    exit 1
fi

if [[ $# -ne 1 ]]; then
    echo "Использование: $0 <VMID>"
    exit 1
fi

VMID="$1"

NODE=$(pvesh get /cluster/resources --type vm \
    | jq -r ".[] | select(.vmid == ${VMID}) | .node")

if [[ -z "${NODE}" ]]; then
    echo "Виртуальная машина ${VMID} не найдена"
    exit 1
fi

echo "VM ${VMID} расположена на узле ${NODE}"
echo "Отправляем команду штатного выключения..."

pvesh create \
    /nodes/${NODE}/qemu/${VMID}/status/shutdown

ELAPSED=0

while true; do
    STATUS=$(pvesh get \
        /nodes/${NODE}/qemu/${VMID}/status/current \
        | jq -r '.status')

    echo "$(date '+%F %T') статус: ${STATUS}"

    if [[ "${STATUS}" == "stopped" ]]; then
        break
    fi

    sleep "${POLL_INTERVAL}"
    ELAPSED=$((ELAPSED + POLL_INTERVAL))

    if (( ELAPSED >= TIMEOUT )); then
        echo "Превышено время ожидания выключения"
        exit 1
    fi
done

echo "Запускаем виртуальную машину..."

pvesh create \
    /nodes/${NODE}/qemu/${VMID}/status/start

sleep 5

STATUS=$(pvesh get \
    /nodes/${NODE}/qemu/${VMID}/status/current \
    | jq -r '.status')

if [[ "${STATUS}" != "running" ]]; then
    echo "Ошибка запуска виртуальной машины"
    exit 1
fi

echo "VM ${VMID} успешно перезапущена"

Что делает этот сценарий

Несмотря на компактность, логика получилась достаточно зрелой.

Сначала выполняется проверка наличия утилиты jq, необходимой для разбора JSON-ответов Proxmox API. Затем определяется VMID, переданный через параметры командной строки.

После этого происходит обращение к кластеру Proxmox для поиска узла, на котором работает виртуальная машина. Это особенно актуально для кластеров, где активно используется миграция.

Далее отправляется команда ACPI Shutdown. Сценарий начинает периодически опрашивать состояние виртуальной машины и ожидает появления статуса stopped.

Если машина не выключается в течение десяти минут, выполнение завершается ошибкой. Такой механизм защищает администратора от бесконечного ожидания в случае зависания гостевой системы.

После успешного завершения работы выполняется запуск виртуальной машины и дополнительная проверка её состояния.

Ограничения Bash-подхода

Несмотря на все улучшения, это всё ещё локальный сценарий.

Он отлично подходит для:

  • аварийных работ;

  • обслуживания отдельных виртуальных машин;

  • быстрых административных операций;

  • небольших домашних лабораторий.

Но если речь идёт о десятках и сотнях виртуальных машин, появляются новые требования:

  • параллельное выполнение;

  • централизованная отчётность;

  • повторный запуск неудачных операций;

  • аудит изменений;

  • интеграция с Git;

  • повторяемость процедур.

И здесь на сцену выходит Ansible.

Перезапуск виртуальных машин через Ansible

Представим ситуацию: необходимо последовательно обслужить сто виртуальных машин.

Можно написать Bash-цикл:

for vm in $(cat vm.list); do
    ./restart.sh "$vm"
done

Но такой подход будет выполняться часами.

Ansible позволяет решить эту задачу значительно эффективнее.

defaults/main.yml

---
proxmox_api_host: proxmox.example.com
proxmox_user: root@pam
proxmox_token_id: ansible
proxmox_token_secret: secret

shutdown_timeout: 600

tasks/main.yml

---
- name: Graceful shutdown
  community.general.proxmox_kvm:
    api_host: "{{ proxmox_api_host }}"
    api_user: "{{ proxmox_user }}"
    api_token_id: "{{ proxmox_token_id }}"
    api_token_secret: "{{ proxmox_token_secret }}"
    vmid: "{{ vmid }}"
    state: stopped
    timeout: "{{ shutdown_timeout }}"

- name: Start VM
  community.general.proxmox_kvm:
    api_host: "{{ proxmox_api_host }}"
    api_user: "{{ proxmox_user }}"
    api_token_id: "{{ proxmox_token_id }}"
    api_token_secret: "{{ proxmox_token_secret }}"
    vmid: "{{ vmid }}"
    state: started

inventory

[proxmox_vms]
vm101 vmid=101
vm102 vmid=102
vm103 vmid=103
vm104 vmid=104
vm105 vmid=105

playbook

---
- hosts: proxmox_vms
  gather_facts: false

  strategy: free
  serial: 20

  roles:
    - proxmox-restart

Параметр serial: 20 ограничивает количество одновременно обрабатываемых машин, а стратегия free позволяет не ждать завершения задач на всех узлах сразу.

В результате можно безопасно перезапускать десятки виртуальных машин параллельно, получать детальный отчёт и повторно запускать только те операции, которые завершились ошибкой.

Bash не умер. Он просто вырос

Инфраструктурный мир любит крайности. Одни пытаются решить любую задачу исключительно Bash-сценариями. Другие готовы поднимать GitOps-платформу и Kubernetes-операторы даже ради перезапуска одной виртуальной машины.

Истина, как обычно, находится где-то посередине.

Bash остаётся отличным инструментом для локальных операций и быстрого реагирования. Но когда инфраструктура вырастает до промышленных масштабов, его естественным продолжением становятся Ansible, декларативные практики и полноценные процессы эксплуатации.

Именно так сегодня выглядит эволюция инфраструктурного кода: небольшой сценарий, написанный однажды для себя, постепенно превращается в роль Ansible, затем становится частью пайплайна, а позже - частью культуры эксплуатации. И это, пожалуй, лучшая новость для Bash. Его никто не убивает. Ему просто находят правильное место.