Удалённое выполнение команд по ssh

20 ноября 2013, 16:57

В свете полученных знаний из ссылок в предыдущем посте решил начать жить по-новому. А именно — бэкапные файлы с фри на фрю складывать не по SMB или FTP, как раньше, а по scp или ssh. Подумал, и решил, что удобнее будет через ssh — туда загоняется поток через pipe, а на удалённой стороне из pipe складывается в локальнный (для удалённого сервера-хранилища бэкапов) файл. Проверил даже скорость — в конкретной сети гиговый файл через SMB скопировался за  1 минуту 48 секунд, а то же самое, но через ssh — за 1 минуту 35 секунд. Профит налицо :-)

Запустил mcedit и начал ваять. Со скриптом бэкапа системы проблем не возникло:

# cat system_backup.sh
#!/bin/sh
backup_dir="/home/backup_operator/backups/os/buhserver-ttr"
keyfile="/home/gateway/service/keyfile.ppk"
username="backup_operator@backup-ttr"

DATE=`date +%Y.%m.%d`

dump -0 -L -u -C 32 -f — / | gzip -5 | ssh -q -i ${keyfile} ${username} "cat > ${backup_dir}/${DATE}_root.dump.gz"
dump -0 -L -u -C 32 -f — /usr | gzip -5 | ssh -q -i ${keyfile} ${username} "cat > ${backup_dir}/${DATE}_usr.dump.gz"
dump -0 -L -u -C 32 -f — /var | gzip -5 | ssh -q -i ${keyfile} ${username} "cat > ${backup_dir}/${DATE}_var.dump.gz"
dump -0 -L -u -C 32 -f — /home | gzip -5 | ssh -q -i ${keyfile} ${username} "cat > ${backup_dir}/${DATE}_home.dump."

keyfile — это private key для авторизации на удалённом сервере, backup_operator — имя пользователя на том сервере, а backup-ttr — имя самого сервера, где у нас бэкапы хранятся. Получается следующее — dump дампит раздел в stdout, откуда это перегоняется в gzip через pipe, откуда перегоняется вот в эту конструкцию:

ssh -q -i ${keyfile} ${username} "cat > ${backup_dir}/${DATE}_root.dump.gz"

Т. е. мы подключаемся к серверу backup-ttr, и выполняем там команду "cat > {backup_dir}/${DATE}_root.dump.gz". Которая читает из stdin сжатый gzip'ом и прокинутый по сети поток и сохраняет его в файл {backup_dir}/${DATE}_root.dump.gz.

В общем, тут всё просто. А вот с бэкапом почты пришлось повозиться. Было принято решение не просто каждый день сваливать полный бэкап почтовой базы и удалять старые копии, а сделать инкрементный бэкап. А для этого нужно производить определённые манипуляции на стороне хранилища бэкапов. Можно, конечно, сделать второй скрипт, и запускать его по крону на стороне хранилища. Но «Шурик, это же не наш метод!». Это придётся контролировать две точки выполнения одной операции — бэкапа почты, так как задействованы будут два скрипта, два крона, два компа и т. д.

Суть задачи такова. Допустим, у нас в день 1 выполняется полный бэкап почтовой базы. В день 2 — только то, что изменилось или добавилось со времени исполнения 1-го бэкапа, отработавшего в день 1. В день 3 — только изменения с момента бэкапа в день 2, и так далее до конца недели. Допустим, за первую неделю у нас сделано 7 архивов, с именами, соответственно, mail1.tgz, mail2.tgz, ... mail7.tgz. Где mail1.tgz — это полная копия базы, а остальные — дельты изменений. Неделя закончилась, надо как-то сделать ротацию бэкапов. То есть, куда-то деть эти 7 архивов. Можно их тупо удалить. Но как-то хочется, чтобы у нас хотя бы двухнедельная история бэкапов хранилась. Самое простое — эти архивы переименовать. Например, по схеме mail1.tgz.bak, mail2.tgz.bak и так далее. Тут-то и началось самое интересное.

Сначала решил сделать как-нибудь так:

for i in `ssh -q -i ${keyfile} ${username} "ls ${backup_dir}/*.gz"`
do
ssh -q -i ${keyfile} ${username} "mv ${i} ${i}.bak"
done

Но это получается 1 вызов ssh для получения списка файлов, и потом ещё в цикле 7 раз запускается ssh, чтобы переименовать каждый отдельный файл. Касичок кагбэ, неоптимальненко.

Камрады в интернете посоветовали сделать как-нибудь так:

ssh -q user@remoteserver 'ls -f /path/*.tgz | while read f; do "$"» };done'

Я попробовал, не прокатило. Полез искать точный синтаксис однострочного оформления циклов в shell. Нашёл несколько примеров, подставил, но ни один не прокатил. Я так понял, что приведённые примеры — в основном для линуксового bash, а у меня фрёвая /bin/sh, поэтому и не прокатывает. Потом нашёл обрывочные сведения, что циклы типа for, foreach, while — «is not single-line friendly». Но коллективный разум мне сурово возразил, что я не прав.

Копаясь дальше, я обнаружил, что реально, однострочные конструкции типа

ls | while read f;do echo ${f};done

работают, будучи запущены в /bin/sh и локально. Но не работают удалённо. Потом до меня дошло, что локально-то я запустил /bin/sh, и в ней пускаю все эти конструкции. Но по дефолту у пользователя backup_operator оболочка прописана /bin/csh, и синтаксис там может отличаться.

Не вопрос, топаем на удалённый сервер и делаем там:

pw usermod backup_operator -s /bin/sh

Меняем, иными словами, ему оболочку на sh вместо csh. После этого конструкция

ssh -i keyfile.ppk backup_operator@backup-ttr "ls /home/ | while read f; do echo ${f}; done;"

просто обязана была заработать. Но не заработала... А написала, что f: Undefined variable.

В процессе дальнейшего общения с коллективным разумом выяснилось следующее:

The shell will expand variables in double-quotes, but not in single-quotes.

что если команду для удалённого исполнения заключать в двойные кавычки, то оболочка подставляет значение переменной вместо имени переменной. А в этой ситуации нужно, чтобы имя переменной передавалось как есть на дальнюю сторону, и значение вычислялось и использовалось уже там. Поэтому эту команду нужно заключать в ОДИНАРНЫЕ кавычки. То есть, вот так:

ssh -i keyfile.ppk backup_operator@backup-ttr 'ls /home/ | while read f; do echo ${f}; done;'

Но есть и другой путь — просто ставить backslash перед символом переменной. То есть, можно сделать вот так:

ssh -i keyfile.ppk backup_operator@backup-ttr "ls /home/ | while read f; do echo \${f}; done;'

Мне так пришлось сделать, потому что по итогу потребовалось, чтобы в одной команде были и локальные и удалённые переменные. Т. е. финальная рабочая конструкция выглядит вот так:

ssh -i keyfile.ppk backup_operator@backup-ttr "cd ${backup_dir}; ls ./ | while read f; do mv \${f} \${f}.bak; done"
Популярное