Есть один любопытный закон развития инфраструктуры. Сначала инженер управляет виртуальными машинами через веб-интерфейс. Затем открывает для себя 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. Его никто не убивает. Ему просто находят правильное место.