Зачем автоматизировать резервное копирование в TrueNAS и что дает этот гайд
Ручное управление ZFS снапшотами в TrueNAS — это не только потеря времени, но и прямой риск потери данных. Человеческий фактор, забывчивость при выполнении рутинных операций, ошибки в командах — всё это может привести к ситуации, когда критически важный снимок не был создан вовремя или старые снапшоты заняли всё свободное пространство. Этот гайд предоставляет готовое, проверенное на практике решение для полной автоматизации жизненного цикла резервных копий — от создания моментального снимка до его репликации на другой сервер и автоматической очистки по заданным правилам.
Вы получите не теорию, а готовые к использованию инструменты:
- Рабочие скрипты на Python и Bash с полной обработкой ошибок для стабильной работы в production-среде.
- Настроенный cron-джоб для запуска процесса по расписанию без вашего участия.
- Систему уведомлений в Telegram или Slack о статусе каждой задачи резервного копирования.
- Гибкие политики очистки старых снапшотов, которые предотвратят захламление дискового пространства.
Использование REST API TrueNAS вместо веб-интерфейса открывает возможности для глубокой интеграции, сложной логики и безошибочного повторения операций. Это решение экономит десятки часов ручной работы в год и значительно снижает операционные риски.
Что мы будем автоматизировать: обзор пайплайна
Весь процесс автоматизации строится вокруг четкого, циклического пайплайна. Каждый шаг будет реализован в коде:
- Создание снапшота: Скрипт отправляет запрос к API TrueNAS для создания моментального снимка указанного датасета. Имя снапшота автоматически включает метку времени (timestamp) для уникальности и удобства сортировки.
- Репликация (опционально): Созданный снапшот может быть реплицирован на другой сервер TrueNAS. Это можно сделать либо через запуск встроенной задачи репликации TrueNAS, либо напрямую командами
zfs send/zfs receiveпо SSH. - Валидация успешности: Скрипт проверяет код ответа API после каждой критической операции (создание, репликация, удаление) и логирует результат.
- Очистка по политике хранения: Автоматически применяется заданная политика ретеншена (например, «хранить последние 30 дневных снапшотов»). Лишние, устаревшие снапшоты удаляются.
- Отправка отчета: По итогам выполнения всего пайплайна отправляется краткое уведомление в выбранный мессенджер с указанием статуса (успех/ошибка), задействованных датасетов и затраченного времени.
Этот замкнутый цикл обеспечивает полную автономность процесса резервного копирования.
Подготовка среды: API ключ, права и тестовая площадка
Перед написанием скриптов критически важно правильно подготовить окружение. Этот шаг предотвратит самые частые ошибки, связанные с правами доступа и аутентификацией, и позволит безопасно протестировать логику на не критичных данных.
Пошаговая инструкция:
- Создание API-ключа в TrueNAS: В веб-интерфейсе перейдите в Account → API Keys и нажмите «Add». Дайте ключу понятное имя, например, «Backup Automation».
- Настройка прав (Privileges): Для нашего пайплайна ключу необходимо предоставить следующие права: Datasets (чтение/запись), Snapshot (полный доступ) и Replication (чтение/запись, если планируется использовать встроенные задачи). Не выдавайте права администратора без необходимости.
- Используйте тестовый датасет: Прежде чем применять скрипты к продакшен-данным, создайте отдельный тестовый пул или датасет (например,
tank/test_backup) для отладки. - Проверка доступности API: Убедитесь, что API доступен с того хоста, где будут запускаться скрипты.
Генерация и безопасное хранение API-ключа TrueNAS
После создания ключ будет показан только один раз. Его необходимо сохранить максимально безопасно. Никогда не храните ключ в открытом виде внутри скриптов, которые могут попасть в системы контроля версий.
Способ 1: Отдельный файл с ограниченными правами (рекомендуется для Bash)
echo "ваш_длинный_api_ключ" > ~/.truenas_api_key
chmod 600 ~/.truenas_api_key # Только владелец может читать и писать
Затем в скрипте ключ можно прочитать так: API_KEY=$(cat ~/.truenas_api_key).
Способ 2: Переменная окружения (удобно для Python, Docker)
Экспортируйте ключ в сессии или добавьте в файл конфигурации оболочки (~/.bashrc, ~/.zshrc):
export TRUENAS_API_KEY="ваш_длинный_api_ключ"
В Python скрипте получите его через os.environ.get('TRUENAS_API_KEY').
Тестовый прогон: создание и удаление снапшота вручную
Перед автоматизацией убедитесь, что базовая связка работает. Выполните эти команды curl, подставив свои значения:
# Замените TRUENAS_HOST, API_KEY и DATASET на свои
TRUENAS_HOST="192.168.1.100"
API_KEY="ваш_ключ"
DATASET="tank/test_backup"
SNAPSHOT_NAME="manual-test-$(date +%Y%m%d-%H%M%S)"
# 1. Создание снапшота
curl -X POST \
-H "Authorization: Bearer ${API_KEY}" \
-H "Content-Type: application/json" \
-d "{\"dataset\": \"${DATASET}\", \"name\": \"${SNAPSHOT_NAME}\"}" \
"https://${TRUENAS_HOST}/api/v2.0/zfs/snapshot"
# В ответе должен быть JSON с id созданного снапшота и кодом 201 (Created).
# 2. Удаление созданного снапшота (очистка)
curl -X DELETE \
-H "Authorization: Bearer ${API_KEY}" \
"https://${TRUENAS_HOST}/api/v2.0/zfs/snapshot/${DATASET}@${SNAPSHOT_NAME}"
Что делать при ошибках:
- 401 Unauthorized: Неверный или просроченный API-ключ. Перепроверьте ключ и его срок действия.
- 403 Forbidden: У ключа недостаточно прав для операции. Проверьте список Privileges в настройках ключа.
- 404 Not Found: Указанный датасет не существует. Проверьте имя датасета (регистр важен).
Успешное выполнение этих команд — сигнал, что среда готова для автоматизации.
Ядро автоматизации: пишем скрипт для управления снапшотами на Python
Python с библиотекой requests — идеальный выбор для сложной логики взаимодействия с API, обработки ошибок и удобного парсинга JSON. Ниже представлен готовый модульный скрипт, который можно адаптировать под ваши нужды.
#!/usr/bin/env python3
import requests
import json
import logging
from datetime import datetime, timedelta
from typing import List, Optional
import os
# ===== КОНФИГУРАЦИЯ =====
TRUENAS_HOST = os.environ.get("TRUENAS_HOST", "192.168.1.100")
API_KEY = os.environ.get("TRUENAS_API_KEY") # Безопасное хранение!
DATASETS_TO_BACKUP = ["tank/data", "tank/vms"] # Список датасетов
RETENTION_DAYS = 30 # Хранить снапшоты за последние N дней
# Настройка логирования
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('/var/log/truenas_backup.log'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
# Базовые заголовки для запросов
HEADERS = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
}
BASE_URL = f"https://{TRUENAS_HOST}/api/v2.0"
# ===== ОСНОВНЫЕ ФУНКЦИИ =====
def create_snapshot(dataset: str, snapshot_name: str) -> bool:
"""Создает снапшот указанного датасета через API TrueNAS."""
url = f"{BASE_URL}/zfs/snapshot"
payload = {
"dataset": dataset,
"name": snapshot_name
}
try:
response = requests.post(url, headers=HEADERS, json=payload, verify=True, timeout=30)
response.raise_for_status() # Вызовет исключение для кодов 4xx/5xx
if response.status_code == 201:
logger.info(f"Снапшот создан: {dataset}@{snapshot_name}")
return True
else:
logger.error(f"Неожиданный статус {response.status_code} при создании снапшота: {response.text}")
return False
except requests.exceptions.RequestException as e:
logger.error(f"Ошибка сети/API при создании снапшота {dataset}: {e}")
return False
except json.JSONDecodeError as e:
logger.error(f"Не удалось разобрать ответ API: {e}")
return False
def apply_retention_policy(dataset: str, keep_days: int = RETENTION_DAYS) -> None:
"""Удаляет старые снапшоты датасета, оставляя только за последние keep_days дней."""
# 1. Получаем все снапшоты датасета
url = f"{BASE_URL}/zfs/snapshot?query=dataset,dataset_name,id,name,properties.creation.rawvalue&dataset_name={dataset}"
try:
response = requests.get(url, headers=HEADERS, verify=True, timeout=30)
response.raise_for_status()
snapshots = response.json()
except requests.exceptions.RequestException as e:
logger.error(f"Не удалось получить список снапшотов для {dataset}: {e}")
return
if not snapshots:
logger.info(f"Для датасета {dataset} не найдено снапшотов.")
return
# 2. Фильтруем и сортируем
cutoff_date = datetime.now() - timedelta(days=keep_days)
snapshots_to_delete = []
for snap in snapshots:
snap_name = snap.get('name')
# Извлекаем timestamp из свойства creation (UNIX epoch)
creation_raw = snap.get('properties', {}).get('creation', {}).get('rawvalue')
if creation_raw:
try:
creation_time = datetime.fromtimestamp(int(creation_raw))
if creation_time < cutoff_date:
snapshots_to_delete.append(snap_name)
except (ValueError, TypeError) as e:
logger.warning(f"Не удалось обработать время создания для {snap_name}: {e}")
# 3. Двойная проверка перед удалением (важно!)
logger.info(f"Для датасета {dataset} найдено {len(snapshots_to_delete)} снапшотов старше {keep_days} дней для удаления.")
if not snapshots_to_delete:
return
# 4. Удаление
deleted_count = 0
for snap_name in snapshots_to_delete:
delete_url = f"{BASE_URL}/zfs/snapshot/{snap_name.replace('@', '%40')}" # Экранирование '@'
try:
del_response = requests.delete(delete_url, headers=HEADERS, verify=True, timeout=30)
if del_response.status_code == 204:
logger.info(f"Удален снапшот: {snap_name}")
deleted_count += 1
else:
logger.warning(f"Не удалось удалить {snap_name}: код {del_response.status_code}")
except requests.exceptions.RequestException as e:
logger.error(f"Ошибка при удалении {snap_name}: {e}")
logger.info(f"Очистка для {dataset} завершена. Удалено: {deleted_count} снапшотов.")
# ===== ТОЧКА ВХОДА =====
if __name__ == "__main__":
if not API_KEY:
logger.critical("API_KEY не установлен. Задайте через переменную окружения TRUENAS_API_KEY.")
exit(1)
overall_success = True
start_time = datetime.now()
for dataset in DATASETS_TO_BACKUP:
snapshot_name = f"auto-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
logger.info(f"Обработка датасета: {dataset}")
# Шаг 1: Создание снапшота
if create_snapshot(dataset, snapshot_name):
# Шаг 2: Применение политики хранения
apply_retention_policy(dataset)
else:
overall_success = False
logger.error(f"Создание снапшота для {dataset} не удалось, очистка пропущена.")
elapsed = datetime.now() - start_time
status = "УСПЕХ" if overall_success else "ОШИБКА"
logger.info(f"Пайплайн завершен. Статус: {status}. Затрачено времени: {elapsed}")
# Здесь позже добавим отправку уведомления
Этот скрипт — готовый каркас. Его можно расширить, добавив аргументы командной строки (argparse) для большей гибкости или интеграцию с системой конфигурации (YAML, JSON).
Альтернатива: Bash-скрипт с curl и jq для быстрого внедрения
Для минималистичных окружений или если вы предпочитаете Bash, вот компактный скрипт, выполняющий те же задачи. Для работы с JSON потребуется утилита jq.
#!/bin/bash
# Конфигурация
TRUENAS_HOST="192.168.1.100"
API_KEY=$(cat ~/.truenas_api_key) # Ключ из защищенного файла
DATASET="tank/data"
RETENTION_DAYS=30
LOG_FILE="/var/log/truenas_backup.log"
# Функция для логгирования
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"
}
# 1. Создание снапшота с timestamp
SNAPSHOT_NAME="auto-$(date +%Y%m%d-%H%M%S)"
log "Создание снапшота для $DATASET..."
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d "{\"dataset\": \"$DATASET\", \"name\": \"$SNAPSHOT_NAME\"}" \
"https://$TRUENAS_HOST/api/v2.0/zfs/snapshot")
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
BODY=$(echo "$RESPONSE" | sed '$d')
if [[ "$HTTP_CODE" -eq 201 ]]; then
log "Снапшот $DATASET@$SNAPSHOT_NAME успешно создан."
else
log "ОШИБКА: Не удалось создать снапшот. Код: $HTTP_CODE. Ответ: $BODY"
exit 1
fi
# 2. Получение списка снапшотов и применение политики хранения
log "Применение политики хранения ($RETENTION_DAYS дней)..."
CUTOFF_TIMESTAMP=$(date -d "-$RETENTION_DAYS days" +%s)
# Получаем список снапшотов и их время создания
SNAPSHOT_LIST=$(curl -s -H "Authorization: Bearer $API_KEY" \
"https://$TRUENAS_HOST/api/v2.0/zfs/snapshot?query=dataset,dataset_name,id,name,properties.creation.rawvalue&dataset_name=$DATASET")
# Используем jq для фильтрации: оставляем только имена снапшотов старше CUTOFF_TIMESTAMP
TO_DELETE=$(echo "$SNAPSHOT_LIST" | jq -r --arg cutoff "$CUTOFF_TIMESTAMP" \
'.[] | select(.properties.creation.rawvalue | tonumber < ($cutoff | tonumber)) | .name')
if [[ -n "$TO_DELETE" ]]; then
log "Найдено снапшотов для удаления: $(echo "$TO_DELETE" | wc -l)"
echo "$TO_DELETE" | while read -r SNAP; do
# Экранируем символ '@' в URL
SNAP_ESCAPED=${SNAP//@/%40}
DELETE_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \
-H "Authorization: Bearer $API_KEY" \
"https://$TRUENAS_HOST/api/v2.0/zfs/snapshot/$SNAP_ESCAPED")
if [[ "$DELETE_CODE" -eq 204 ]]; then
log "Удален: $SNAP"
else
log "Предупреждение: не удалось удалить $SNAP (код: $DELETE_CODE)"
fi
done
else
log "Нет снапшотов для удаления по заданной политике."
fi
log "Скрипт завершил работу успешно."
Плюсы и минусы подхода на Bash:
- Плюсы: Легковесность, не требует установки интерпретатора Python, отлично подходит для простых сценариев и cron.
- Минусы: Сложнее реализовать продвинутую обработку ошибок, вложенную логику и модульность. Работа с JSON через
jqможет быть менее интуитивной для сложных структур.
Выбор между Python и Bash зависит от сложности ваших задач и личных предпочтений. Для начала можно использовать Bash-скрипт, а по мере роста потребностей переписать логику на Python.
Настройка репликации ZFS снапшотов между системами
Репликация — ключевой элемент отказоустойчивости. Она создает географически распределенную копию ваших данных. Мы рассмотрим два проверенных метода, каждый из которых можно интегрировать в наш пайплайн.
Сравнение подходов:
- Встроенные задачи репликации TrueNAS (Replication Task): Высокоуровневый, оптимизированный механизм. Управляется через веб-интерфейс или API. Поддерживает инкрементальную передачу, сжатие, шифрование и валидацию данных. Идеален для репликации между серверами TrueNAS.
- Прямая репликация через
zfs send|zfs receive: Низкоуровневый, гибкий метод. Работает между любыми системами, поддерживающими ZFS (Linux, FreeBSD). Требует настройки SSH-ключей и более глубокого понимания команд ZFS.
Запуск задачи репликации TrueNAS через API
Если у вас уже настроена задача репликации в TrueNAS, её можно запускать по расписанию через API, что удобно для интеграции в общий пайплайн после создания снапшота.
1. Найдите ID задачи репликации:
curl -s -H "Authorization: Bearer $API_KEY" \
"https://$TRUENAS_HOST/api/v2.0/replication" | jq '.[] | select(.name=="Ваша_Задача_Репликации") | .id'
2. Запустите задачу:
REPLICATION_TASK_ID=1 # Подставьте ваш ID
curl -X POST \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
"https://$TRUENAS_HOST/api/v2.0/replication/run" \
-d "{\"id\": $REPLICATION_TASK_ID}"
3. Мониторинг статуса: Запрос к /api/v2.0/replication?id=$REPLICATION_TASK_ID покажет состояние задачи (state: SUCCESS, FAILED, RUNNING).
Прямая репликация через SSH (пример для Bash):
Этот метод требует предварительной настройки аутентификации по SSH-ключам между серверами.
# На сервере-источнике (где запускается скрипт)
REMOTE_HOST="backup-server.local"
REMOTE_USER="root"
REMOTE_DATASET="backup/tank_data"
LOCAL_SNAPSHOT="tank/data@auto-20260409-120000"
# Инкрементальная отправка последнего снапшота
# Предполагаем, что предыдущий общий снапшот называется tank/data@parent
zfs send -I tank/data@parent "$LOCAL_SNAPSHOT" | \
ssh "$REMOTE_USER@$REMOTE_HOST" zfs receive -F "$REMOTE_DATASET"
Для надежности добавьте проверку кода выхода ($?) после каждой команды.
Запуск по расписанию и надежный мониторинг (cron + Telegram/Slack)
Чтобы пайплайн работал полностью автономно, его нужно добавить в планировщик cron и настроить уведомления о результатах.
1. Настройка cron-доба
Откройте crontab для редактирования: crontab -e.
Добавьте строку для ежедневного запуска в 2:00 ночи с логированием вывода:
# Запуск Python-скрипта каждый день в 2:00
0 2 * * * /usr/bin/python3 /path/to/your/truenas_backup.py >> /var/log/truenas_backup_cron.log 2>&1
# Или для Bash-скрипта
# 0 2 * * * /bin/bash /path/to/your/truenas_backup.sh >> /var/log/truenas_backup_cron.log 2>&1
Важно: Cron запускает задачи в минимальном окружении. Убедитесь, что в скрипте используются абсолютные пути к бинарникам (/usr/bin/python3, /usr/bin/curl) и что переменные окружения (особенно TRUENAS_API_KEY) установлены глобально (например, в /etc/environment) или заданы прямо в crontab.
2. Интеграция уведомлений в скрипт
Модифицируйте основной скрипт (Python), добавив функцию отправки отчета.
Интеграция с Telegram Bot API: код и настройка бота
Telegram — один из самых простых и популярных способов получения уведомлений.
Настройка бота (5 минут):
- Найдите в Telegram @BotFather, отправьте команду
/newbotи следуйте инструкциям. - После создания бота BotFather предоставит токен (например,
1234567890:ABCdefGHIjklMNOpqrsTUVwxyz). Сохраните его. - Найдите своего бота в Telegram и отправьте ему любое сообщение (это нужно для активации).
- Чтобы получить chat_id, отправьте GET-запрос (в браузере или curl):
https://api.telegram.org/bot<ВАШ_ТОКЕН>/getUpdates
В ответе JSON найдитеmessage.chat.id— это ваш числовой chat_id.
Код функции для Python:
import requests
def send_telegram_notification(message: str, token: str, chat_id: str) -> bool:
"""Отправляет сообщение в Telegram чат через бота."""
url = f"https://api.telegram.org/bot{token}/sendMessage"
payload = {
"chat_id": chat_id,
"text": message,
"parse_mode": "HTML"
}
try:
response = requests.post(url, json=payload, timeout=10)
return response.status_code == 200
except requests.exceptions.RequestException:
return False
# Конфигурация (лучше хранить в переменных окружения!)
TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN")
TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID")
# В конце основного скрипта, после завершения пайплайна:
status_msg = f"<b>Резервное копирование TrueNAS</b>\n"
status_msg += f"Статус: {status}\n"
status_msg += f"Датасеты: {', '.join(DATASETS_TO_BACKUP)}\n"
status_msg += f"Затрачено времени: {elapsed}\n"
status_msg += f"Время завершения: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
if TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID:
send_telegram_notification(status_msg, TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID)
Для Slack процесс аналогичен: создайте Incoming Webhook в настройках Slack-канала и отправляйте POST-запросы на полученный URL.
Отладка и решение частых проблем
Даже с готовыми скриптами могут возникнуть проблемы при внедрении. Этот чек-лист поможет быстро их диагностировать и устранить.
- Ошибка 401/403 при запросах API:
- Проверьте, что API-ключ скопирован полностью, без лишних пробелов.
- Убедитесь, что срок действия ключа не истек (в TrueNAS можно задать срок).
- Перепроверьте права (Privileges) ключа в веб-интерфейсе TrueNAS.
- Скрипт работает из консоли, но не из cron:
- Самая частая причина — разное окружение. Cron не загружает ваш
.bashrcили.profile. Либо задайте переменные (TRUENAS_API_KEY,PATH) прямо в crontab, либо используйте абсолютные пути ко всем исполняемым файлам (/usr/bin/python3,/usr/bin/curl). - Проверьте права на исполнение скрипта:
chmod +x /path/to/script.py. - Смотрите логи cron:
grep CRON /var/log/syslogилиjournalctl -u cron.
- Самая частая причина — разное окружение. Cron не загружает ваш
- Не удаляются старые снапшоты:
- Убедитесь, что политика именования снапшотов в скрипте совпадает с теми, что ищет функция очистки. Если вы создаете снапшоты с префиксом
auto-, а очистка ищет все — проблем не будет. - Проверьте логику работы с датами в скрипте. Убедитесь, что временная зона (timezone) обрабатывается корректно.
- Для Bash-скрипта проверьте, что
jqкорректно извлекает полеproperties.creation.rawvalue.
- Убедитесь, что политика именования снапшотов в скрипте совпадает с теми, что ищет функция очистки. Если вы создаете снапшоты с префиксом
- Репликация не запускается или падает:
- Проверьте сетевое соединение и разрешения брандмауэра между серверами (порты 22 для SSH, 443 для API).
- Для SSH-репликации убедитесь, что аутентификация по ключам работает без запроса пароля.
- Для встроенных задач репликации проверьте, что задача активна (не отключена) и у неё указан корректный удаленный хост и данные для аутентификации.
- Совместимость и актуальность:
- Представленные скрипты и API-запросы проверялись и актуальны для TrueNAS CORE 13.0+ и TrueNAS SCALE 22.02+ (Angelfish) и более поздних версий.
- API TrueNAS развивается. Если вы столкнулись с ошибкой «endpoint not found», проверьте актуальную документацию по API для вашей версии. Основные эндпоинты (
/zfs/snapshot,/replication) остаются стабильными. - Для работы со сложными сценариями передачи данных, например, выгрузкой готовых резервных копий на внешние FTP-серверы, вам могут пригодиться готовые скрипты для автоматической передачи файлов.
Этот пайплайн — фундамент для построения надежной, автоматизированной системы резервного копирования на базе TrueNAS. Начните с тестового датасета, убедитесь, что все этапы работают, а затем смело переносите решение на продакшен-окружение. Автоматизация освободит ваше время для решения более сложных задач и даст уверенность в сохранности данных.