Вариант простой backup-системы на Python, Bash и Git

    Недавно появилось некоторое чувство дискомфорта когда я приступаю к работе. Чувство было не то чтобы сильным, но сосредоточиться мешало. Думал, лень. Оказалось, что все чуть сложнее :) Ноуту, за которым я работаю, уже почти 3 года; стоит на нем Mac OS X 10.6.1, но яблок на нем нигде не нарисовано, и система периодического резервного копирования на нем отсутствует как класс. В общем, не было ощущения стабильности и надежности, так что я занялся этим вопросом вплотную. Собственно, далее я опишу результат, который мое подсознание удовлетворил :) Может быть, кому-то что-нибудь будет полезно.


    Задача


    Что бекапим


    • Каталоги с разнообразными исходниками. Некоторые проекты являются Git-репозиториями, соответственно, хочется сохранить репозиторий, а не просто последнюю версию исходников. NB: выполнять make clean или аналоги в конце дня я как-то не привык, так что добавляем требование, что бинарники не копируются.
    • Каталоги с конфигами и параметрами.
    • Каталоги с TeX-исходниками. На самом деле, можно было бы добавить к первому пункту.

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

    Куда бекапим


    • Собственно, файлохранилища. Был выбран бесплатный аккаунт на Dropbox, ибо копировать командой cp – просто и удобно.
    • Почта на Gmail. Все равно 7 гигов не используются, да и в надежности пока не было поводов сомневаться.

    Возможно, в ближайшем будущем этот список расширится, а пока, как мне кажется, этого более-менее достаточно.

    Какие фичи


    • Простота. Основная фича :) Не нужно сложных и больших программ, я сам в состоянии решить какие файлы откуда мне нужно копировать. Проще быть уверенным в корректности работы небольшой программы.
    • История изменений. Первая фича, которую хочется – это история изменений, чтобы не ругаться на тему «чем же этот файл мне мешал!»
    • Сжатие. Git, конечно, жмет, но т.к. хранить бекапы все равно удобнее в одном файле, так что пускай его дожмет кто-нибудь еще.
    • Шифрование Я не сомневаюсь в честности и секъюрности провайдеров интернета и провайдеров хранилищ. Тем не менее, не хочу, чтобы то, что я делаю, передавалось и лежало непонятно где в открытом виде.


    Решение


    Гугл по моим запросам ничего путного и удовлетворяющего моим запросам не выдал, поэтому пришлось писать самому. Заодно потрогал что есть Python, давно искал подходящий повод.

    Копирование


    Вариант «копировать все» не проходит из-за нехватки места. К тому же, как-то кажется глупым держать кучу резервных копий ненужных файлов (объектников, логов сборки и т.п.). В итоге, так сказать, техзадание получилось следующим:
    • Список каталогов и файлов для копирования хранится в отдельном файле.
    • Для каждого каталога можно указать маски файлов, которые из него нужно копировать. Если маска не указана, копировать все.
    • Сохраняется исходная структура каталогов.
    • Скрипт копирует (и только копирует) файлы в указанную папку.
    • Если в целевой папке файл уже существует, копировать только в случае, если время модификации исходного файла больше.


    Скрипт копирования написан на Python и занимает чуть меньше 200 строк (с учетом отступов, комментариев и т.п.). Дополнительно использует один нагугленный модуль для преобразования абсолютных путей в относительные и наоборот.
    Использование:
    sn-backup.py <файл со списком> <базовый каталог> <каталог для резервного копирования>

    Ограничения:
    1. Строгий формат файла со списком. Формат строки:
      <путь относительно базового каталога>[\t<маски через запятую>]

      Более одного знака табуляции не допускается. Маски идут через запятую без пробелов.
    2. Практически не реализована защита от дурака.


    Пример списка (с базовым каталогом ~):
    .emacs
    Documents/Programming	*.c,*.h,*.cpp,*.pro,*.py,.git
    Scripts
    


    Исходники (наверное, удобнее когда на файлохостинге лежат, чем когда те самые 200 строк засоряют текст):
    sn-backup.py (посмотреть на dumpz.org)
    relpath.py (посмотреть на dumpz.org)

    relpath.py был взят отсюда, из первого коммента, и чуть подправлен.

    Сжатие и шифрование


    Сжатие и шифрование требуют по одной строчке, так что как-то странно было бы их писать на Python. Чем сжимать – дело вкуса. Я предпочитаю 7-zip. Командная строка тут, соответственно, будет простой (имя архива – backup.7z, имя папки – .backup):
    7za a -mx7 backup.7z .backup

    Для шифрования выбрал openssl, так как, в теории, почти везде есть и работает из коробки, без необходимости генерации чего бы то ни было. Команда выглядит так:
    openssl enc -aes-256-cbc -salt -in backup.7z -out backup.aes256cbc -pass file:<pass path>

    Соответственно, имя исходного файла backup.7z, имя выходного файла backup.aes256cbc (на случай, если вдруг забуду как зовется алгоритм шифрования). Пароль хранится в файле и может иметь произвольную длину. Лучше, все же, не менее 32 символов.

    Отправка


    Отправку, из-за потенциального разнообразия способов, сначала хотелось сделать тоже через очень настраиваемый Python-скрипт, но потом я решил, что копировать на небольшое число хостов гораздо проще вручную, а копировать на большое число хостов попахивает паранойей. Поэтому отправка реализована в sh-скрипте. Команда копирования на Dropbox при установленном клиенте интереса не представляет, так что рассмотрим только команду отправки на почту (она хоть чуть реже встречается):
    uuencode <путь к зашифрованному архиву> $(basename <путь к зашифрованному архиву>) | mail -s "[BACKUP] $(date)" <адрес>

    Внимание: чтобы эта команда работала, необходимо настроить на локальной машине SMTP-сервер. У меня провайдер его предоставляет Команда прикрепляет зашифрованный архив к сообщению с темой «[BACKUP] <текущая дата и время>» и отправляет сообщение на <адрес>. У меня эта команда лежит в скрипте sn-upload.sh, который в качестве единственного аргумента принимает имя файла зашифрованного архива.

    Все вместе


    Все вместе собирается скриптом backup.sh, который выглядит примерно так:
    # ~/.backup - каталог, в который и происходит резервное копирование.
    # Предварительно в нем должен быть инициализирован .git-репозиторий.
    cd ~/.backup

    # Список удобно держать там же.
    ~/Scripts/sn-backup.py ./list ~ .

    # Скопировали, теперь коммитим в git.
    git add .
    git commit -m "Backup at $(date)"

    # Возвращаемся назад и архивируем.
    cd ..
    7za a -mx7 backup.7z .backup > /dev/null

    # Шифруем.
    openssl enc -aes-256-cbc -salt -in backup.7z -out backup.aes256cbc -pass file:///${SECRET_PATH}/.password

    # Отправляем.
    ~/Scripts/sn-upload.sh ./backup.aes256cbc

    # ???
    rm backup.7z
    # PROFIT!


    Направления для дальнейшего развития


    На досуге, возможно, сделаю такие доработки:
    1. Перевести все bash-скрипты на Python.
    2. Возможно, переключиться на pylzma и встроенную криптографию Python (для кроссплатформенности).
    3. Сделать формат файла-списка помягче и поприличнее.


    Заключение


    Я не исключаю возможности, что я изобрел велосипед. Но я потратил на это не так много времени, и получил немного опыта. Хочу отметить, что прежде всего в, кхм, дизайн этой системы закладывалась простота. Я знаю какие файлы и откуда мне надо скопировать. Я знаю что с ними потом делать. Мне нужен скрипт только для того, чтобы все это делалось на автомате. На мой взгляд, свои задачи он выполняет полностью.

    Благодарю за внимание!

    Комментарии 7

      0
      Спасибо за статью. Я использвал похожую технику, она мне тоже приятна и я стал её применять на многих хостах. Вот в этот-то момент меня и заломало настраивать взеде локальный SMTP :-) Текомендую для отправки почты msmtp. Она поддежривает все необходимы фичи, всякие там TLS и прочее. Вот конфиг для гуглового почтового сервера.
      $ cat .msmtprc 
      account default
      host smtp.gmail.com
      port 587
      from XXX@XXX.XX
      tls on
      tls_starttls on
      tls_certcheck off
      auth on
      user XXXXXXX@gmail.com
      password XXXXXXX
      logfile /var/log/msmtp.log
        0
        Спасибо за дополнение! :)
        Сам потратил много времени в попытках настроить встроенный Postfix на Gmail, пока не догадался узнать насчет SMTP-сервера провайдера.
      • НЛО прилетело и опубликовало эту надпись здесь
          0
          Ну, во-первых, возможно, при таких размерах backup-а лучше использовать уже более сложные программы. Или, скажем, синхронизировать непосредственно Git-репозиторий с находящимся на доверенных серверах.

          Во-вторых, для размеров подобного порядка можно оптимизировать примерно так:
          Делаем 2 скрипта, backup-monthly.sh и backup-daily.sh, добавляем их в crontab для выполения ежемесячно и ежедневно соответственно. Ежемесячный скрипт выглядит так же, только не удаляем .7z в конце; в ежедневном меняем команду сжатия 7-zip на инкрементальное (наверное, можно накопать в мануале 7-zip или в гугле; навскидку, из того же гугла, как-то так:

          7za u backup.7z -up0q3x2z0!backup-delta-$(<тут вычисляется какой-нибудь уникальный порядковый номер>).7z * -ms=off

          ). Шифруем и отправляем, естественно, delta-файл. Единственное «но»: я не занимался вопросами того, как потом это дело распаковывать и можно ли делать архивы действительно инкрементальными (т.е. по алгоритму: сделали дельту, отправили дельту, слили изменения с локальной копией исходного backup.7z, чтобы следующая дельта была уже от текущего состояния).
          0
          rsync?
            0
            Вот прошу прощения, нет сейчас возможности глубоко покопаться в документации, но к rsync (в сравнении с моим sn-backup) потенциально есть такие вопросы:
            • Поддерживается ли список масок при копировании, или то, что в примере, придется разбивать на 6 строк?
            • Поддерживается ли копирование по маске со сканированием всего поддерева (тут подозреваю, что возможно)?
            • Два предыдущих вопроса одновременно?..
              0
              Поддерживает и то и другое и ещё много чего.

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое