Ключевые слова:shell, expect, perl, tcl, python, (найти похожие документы)
From: Mikhail E. Zakharov <zakharov@ipb.redline.ru.>
Newsgroups: email
Date: Mon, 17 May 2005 14:31:37 +0000 (UTC)
Subject: Интерактивные программы в скриптах UNIX
Как ни крути,
но рано или поздно становится понятно, что shell-скрипты для
администрирования UNIX писать все же придется. В том числе и те,
которые будут взаимодействовать с интерактивными программами такими,
как telnet, ftp, su, passwd, ssh. Но тогда можно быть уверенным, что
спокойной жизни администратора наступил конец, поскольку
интерактивность программ таит в себе множество подводных камней,
которые не встречаются в программировании рядовых shell-скриптов. Хотя
к счастью, или нет, большинство этих проблем традиционно проявляются в
течение первых пяти минут и выглядят для автора скрита как
невозможность пройти аутентификацию на сервере. И поначалу это
обстоятельство ставит в тупик, поскольку привычные конвейерные
конструкции вида:
$ echo luser && echo TopSecret | telnet foo.bar.com
вроде бы срабатывать отказываются и в результате простая, на первый
взгляд, проблема приобретает статус невыполнимого задания. Однако, не
все потеряно, и если не вдаваться в суть проблемы, хотя мы это чуть
позже обязательно сделаем, простое решение появится довольно быстро,
ведь многие из диалоговых программ имеют встроенные механизмы
обработки скриптов, как например, стандартный ftp-клиент FreeBSD:
$ echo '$FILEPUT' | ftp -N ftprc [email protected]
Эта команда заставит ftp подключиться к хосту foo.bar.com под именем
пользователя luser и выполнить макрос FILEPUT (закачка файла на
сервер) описанный в файле ftprc. В этом же файле кроме макроса, должны
быть обязательно описаны хост, логин и пароль пользователя на этом
хосте:
$ cat ftprc
machine foo.bar.com
login luser
password TopSecret
macdef FILEPUT
binary
cd /tmp
put some_usefull_file.bin
bye
<-- Внимание! Здесь обязательный перевод строки в конце макроса.
$
Если же диалоговая программа по какой-то причине не поддерживает
встроенные скрипты, то среди свободно-распространяемого ПО всегда
найдется ее аналог, в котором уже давно реализована возможность
выполнять действия автоматически. В самом деле, вы же не первый
столкнулись с подобной задачей!
Другой способ заставить диалоговую программу выполнить что-либо без
участия пользователя, это перенаправить ее стандартный ввод. Например,
так в сильно упрощенном виде выглядит скрипт загрузки Oracle:
#!/bin/sh
su - oracle -с /oracle/bin/svrmgrl <<EOF
connect internal
startup
EOF
Однако этот способ не помощник при работе с программами типа telnet
поскольку он не защищен от пресловутой "проблемы аутентификации
пользователя". Ситуация к тому же осложняется трудностями отладки
скриптов в момент этой самой аутентификации. И понятно, что для
разрешения таких ситуаций требуется какое-то специальное решение,
поиск которого, естественно, традиционно осуществляется в интернете.
Беглое штудирование поисковых систем на предмет проблемы принесет
плоды в виде невнятных бормотаний по поводу, преждевременного закрытия
входного потока данных, псевдо-терминалов (pty) и прочее, но среди
всего этого бестолкового обилия обязательно будут присутствовать
ссылки и на expect.
expect
это как раз именно то, что и требовалось найти: утилита, которую
традиционно используют для автоматизации работы с интерактивными
программами. И как это обычно бывает, к сожалению имеется и одно
маленькое неудобство: для работы expect необходим еще один
установленный в системе язык программирования - TCL. Правда если
инсталляция и изучение еще одного, правда очень могучего, языка вас не
смущает, то на этом свои поиски можно прекращать: для написания
скриптов expect и TCL/TK имеют все необходимое и даже больше.
Если в вашей системе expect отсутствует, то его следует
инсталлировать. Во FreeBSD это удобно сделать используя систему
портов:
# cd /usr/ports/lang/expect
# make install clean
В результате expect и все что ему необходимо буде скачано из интернет
и установлено в системе.
Теперь с TCL и expect можно работать и применительно к нашей проблеме
"интерактивности" expect-скрипт описывающий короткую telnet-сессию с
FreeBSD на хост foo.bar.com (пусть это будет SCO UnixWare-7.1.3) под
именем пользователя luser с паролем TopSecret может выглядеть примерно
так:
#!/usr/bin/expect
spawn telnet foo.bar.com
expect ogin {send luser\r}
expect assword {send TopSecret\r}
send "who am i\r"
send "exit\r"
expect eof
В принципе, в README к expect сказано, что существует библиотека
libexpect, которую можно использовать при написании программ на C/C++
избежав при этом использования самого TCL. Но скорее всего, эта тема
не вписывается в рамки данной статьи, да и сами авторы expect
склоняются к мысли, что проще использовать скрипты expect, а не
библиотеку.
Однако, если несмотря на всю привлекательность вышеизложенного метода
и уговоры авторов из FAQ по expect, вы решили не использовать expect,
значит вы либо слишком ленивы, либо ваша душа уже насквозь отравлена
языком Perl. Чтож, в этом случае ваше спасение в установке
соответствующего модуля Perl
(http://sourceforge.net/projects/expectperl), который призван
обеспечить функционал оригинального expect'а. Под FreeBSD это можно
осуществить знакомым методом установки из портов:
# cd /usr/ports/lang/p5-Expect
# make install clean
Теперь наш пример с telnet-сессией будет выглядеть так:
#!/usr/bin/perl
use Expect;
my $exp = Expect->spawn("telnet foo.bar.com");
$exp->expect($timeout,
[ 'ogin: $' => sub {
$exp->send("luser\n");
exp_continue; }
],
[ 'assword:$' => sub {
$exp->send("TopSecret\n");
exp_continue; }
],
'-re', qr'[#>:] $'
);
$exp->send("who am i\n");
$exp->send("exit\n");
$exp->soft_close();
Если же я ошибся, в вашей приверженности языку Perl, у вас всегда есть
возможность использовать Python для которого написан соответствующий
модуль pexpect (http://pexpect.sourceforge.net). Язык Python,
естественно, уже должен быть проинсталлирован в системе заранее. Если
нет, порты FreeBSD нас снова выручат:
# cd /usr/ports/lang/python
# make install clean
И, соответственно, для модуля pexpect:
# cd /usr/ports/misc/py-pexpect
# make install clean
Скрипт telnet-сессии на Python будет таким:
#!/usr/local/bin/python
import pexpect
child = pexpect.spawn('telnet foo.bar.com');
child.expect('ogin: ');
child.sendline('luser');
child.expect('assword:');
child.sendline('TopSecret');
child.sendline('who am i');
child.sendline('exit');
child.expect(pexpect.EOF);
print child.before;
Конечно, если и Python вас по какой-то причине не устраивает, тогда вы
можете установить, например, PHP. Ну, в общем, вы поняли, поиск
удобных вариантов решений продолжать можно довольно долго, и разве что
только Visual Basic'а в этом списке не будет. Поэтому думаю, настало
время, интересное это занятие отложить в сторону, и наконец,
попытаться понять скрытую суть вещей.
Суть вещей
Итак, при запуске интерактивной программы из shell-скрипта на самом
деле происходит следующее... И хотя эта фраза звучит в стиле
заключительной речи сыщика Пуаро, до конца повествования на самом деле
еще довольно далеко и имеет смыс начать издалека, т.е. с самого
начала. А потому, просто необходимо забыть все те красивости, которые
предлагаются expect'ом или его, в высшей степени, достойными клонами.
Для начала попытаемся с имитировать некое грубое подобие expect-like
программы при помощи скриптов shell с использованием всего арсенала
стандартных утилит *NIX-системы и попытаемся таки хоть отчасти уловить
то самое, что указано в заголовке раздела. Важно, также отметить, что
все эксперименты справедливы для FreeBSD и нет никаких гарантий, что
они дадут те же результаты и на других операционных системах.
Чтобы максимально упростить нашу задачу, и не бороться с конвейерами,
упомянутыми в первых примерах в самом начале статьи, создадим два
fifo-файла, один для стандартного ввода (in.fifo), другой для вывода
(out.fifo), и бог с ним пока со стандартным потоком ошибок:
$ mkfifo in.fifo
$ mkfifo out.fifo
Далее, запустим неудачный в плане безопасности, и, из-за примеров на
expect, perl и python, досмерти надоевший telnet с перенаправлениями
потоков ввода-вывода на in.fifo и out.fifo:
$ telnet -K localhost > out.fifo < in.fifo
Маленький шаг в сторону: поскольку все эксперименты проводятся на
FreeBSD, то при попытке подключения к telnet-серверу, тоже
загруженному на FreeBSD, автоматически включаются механизмы (SRA),
призванные обеспечить безопасность telnet-сессии:
$ telnet localhost
Trying ::1...
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Trying SRA secure login:
User (luser): <-- ожидание ввода логина (строка не переведена)
Чтобы отключить такое поведение и вернуть telnet'у традиционный вид,
будем загружать telnet с ключем -K:
$ telnet -K localhost
Trying ::1...
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
FreeBSD/i386 (unity) (ttyp1)
login: <-- ожидание ввода логина (строка не переведена)
Итак, пусть наш первый скрипт test_1.sh пока состоит из следующих
строк:
#!/bin/sh
mkfifo in.fifo
mkfifo out.fifo
telnet -K localhost > out.fifo < in.fifo
Теперь запустив скрипт на исполнение:
$ ./test_1.sh &
можно начинать проводить эксперименты. Обратите внимание на параметр
&, переводящий скрипт в фоновое выполнение команды, это сделано для
того, чтобы иметь возможность продолжать работу с тем же терминалом во
время наших опытов. Например, попробуем прочитать что-либо из out.fifo
и написать что-либо в in.fifo.
$ cat out.fifo &
Параметр здесь & указан по той же самой причине, что и в случае с
вызовом скрипта. Правда в результате выполнения команды cat на экране,
к сожалению, ничего не появится. Видимо, это происходит из-за
блокировок чтения/записи при работе fifo-файлов: запись в fifo-файл
блокируется пока из него никто не читает, и чтение из fifo-файла тоже
блокируется, пока другая сторона не готова в него писать. Вероятно,
весь процесс тормозит наш in.fifo, в который ничего не записано.
Проверим эту догадку послав перевод строки в input-канал:
$ echo > in.fifo
Была ли догадка правильной, или истинная причина скрывается в чем-то
другом, но чудо таки произошло! На терминале появились долгожданные
результаты работы команды cat out.fifo:
$ Trying ::1...
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
FreeBSD/i386 (unity) (ttyp1)
login: login:
Единственное, что смущает, так это дважды повторяющиеся login:. На
самом деле, это естественная реакция telnet-сервера на то, что выше
командой: echo > in.fifo ему был послан перевод строки.
Сделав необходимый вывод, чтобы избежать отсылки перевода строки,
заменим команду echo > in.fifo на:
$ echo -n > in.fifo
А еще лучше, будет использовать комбинацию:
$ cat > in.fifo &
Это должно в будущем предотвращать закрытие канала ввода, что
справедливо воспринимается telnet'ом, как разрыв связи.
Чтож, теперь можно продолжать наши изыскания, но для начала следует
избавиться от background-процессов, которые появились в результате
использования &:
$ fg
./test_1.sh
^C[2] + Done cat out.fifo
Команда fg и затем нажатие ^C на клавиатуре для того, чтобы прервать
работу процесса.
Итак, следующим шагом будет попытка отфильтровать выходной поток
(out.fifo), чтобы исходя из заданного шаблона "отловить" необходимые
данные чтобы реализовать основную комбинацию команд expect:
expect request {send answer}
Сама собой напрашивается какая-нибудь такая связка:
$ cat out.fifo | grep request && echo answer
Однако, в нашем случае цепочка не будет правильно срабатывать уже на
этапе grep. Это объясняется тем, что grep предназначен для распечатки
строк, соответствующих указанному шаблону, и поэтому совершенно
естественно, что перед началом сравнения данного шаблона с каждой
новой строкой, grep всегда ожидает окончания ввода этой строки (\n)
или же окончания файла (\0). В нашем случае эта особенность делает
невозможным перехват login: при помощи grep поскольку за login: не
следует перевод строки (система ожидает от пользователя ввод логина),
этот момент выше отражен на примере telnet -K localost. Таким образом
grep будет вечно (до истечения таймаута у telnet-сервера) ожидать
получения конца строки, а затем увидит сразу конец файла.
Необходим другой механизм обработки таких незаконченных строк. Как
вариант, возможно вместо cat использовать команду dd, которая в цикле
будет посимвольно передавать данные grep'у. Я имею ввиду следующую
конструкцию, которая показана на примере уже работающего скрипта
expect.sh:
#!/bin/sh
while :; do
dd if=out.fifo bs=1b count=1 2>/dev/null | grep $1
if [ $? -eq 0 ]; then
# Match found
echo "$2" > in.fifo
exit 0
fi
# Match not found, let's play again
done
Запускать скрипт можно следующим образом (файлы in.fifo и out.fifo уже
должны быть созданы):
$ ./expect.sh "request" "answer"
Чтож, настало время собрать воедино в одном скрипте (test_2.sh) весь
полученный опыт и попытаться автоматизировать нашу традиционную
telnet-сессию:
#!/bin/sh
mkfifo out.fifo in.fifo
telnet -K localhost 1> out.fifo 0< in.fifo &
cat > in.fifo &
cat out.fifo > out.fifo &
pid=`jobid`
./expect.sh "ogin" "zmey"
./expect.sh "word" "SaracSh"
sleep 1
echo 'who am i > /tmp/test.txt' > in.fifo
sleep 1
echo "exit" > in.fifo
rm out.fifo in.fifo
kill $pid
Запустив этот скрипт на выполнение, если все удачно, получится
следующий незамысловатый вывод:
$ ./test_2.sh
login:
Password:
Connection closed by foreign host.
Зато в качестве боевого трофея останется файл /tmp/test.txt, который
будет подтверждать успешность эксперимента:
$ cat /tmp/test.txt
luser ttyp3 May 10 16:39 (localhost)
Если же что-то пошло неудачно, вероятно придется воспользоваться
командой kill для каждого процесса, оставшегося от провалившегося
опыта.
К сожалению, скрипт этот работает крайне нестабильно: он сильно
зависит от величин параметра sleep и скорости ответа telnet-сервера
из-за чего не всегда данные приходят вовремя, что ведет к зависаниям и
процессы приходится часто убивать. Ясно также, что такая конструкция
работает не везде, например в Linux мне не удалось добиться хоть
сколько-нибудь приемлемых результатов, но если это интересно, путь
любители Linux'а этим и занимаются.
Надо идти дальше
и найти утилиту, решил я, которую можно было бы без всяких надстроек
типа TCL, Perl, Python использовать в качестве expect-заменителя для
чистого shell. Ясно, что написана она должна быть на С и быть
портирована под возможно большее количество операционных систем. Чтож,
let's Google! И через некоторое время такая программа нашлась, это
оказалась pty-4.0 написанная Daniel J. Bernstein в 1992 году. Однако,
после этого релиза, программа, судя по всему, больше не развивалась.
Через некоторое время исходный код pty-4.0 у меня даже собрался в
бинарный, и некоторые части pty-4.0 стали запускаться. Но к этому
моменту стало очевидно, что проще написать свою программу, чем
разбираться со старой.
А почему бы и не написать? И я углубился в более серьезное изучение
вопроса. Довольно быстро я узнал, что, действительно, наиболее удобный
способ взаимодействия с интерактивными программами, это именно
имитация для них терминала. И место псевдо-терминалов в структуре
будущей программы четко определилось несмотря даже на сбивчивые
бубнения специалистов из интернета насчет expet'а и pty-сессий. Стало
также понятно, что метод запуска приложений под контролем pty-сессий
очень удобно выполнять внутри какой-либо оболочки, например, TCL, Perl
и пр. Однако для тех же целей, ничего не мешает использовать программу
на С и чистый интерпретатор sh.
В результате всего этого, уже через пару недель у меня была готова
рабочая версия empty (http://www.sourceforge.net/projects/empty),
которая позволяет запускать интерактивные программы и вести с ними
диалог посредством fifo-файлов. Например, надоевшая FreeBSD сессия
telnet в варианте shell-скрипта для empty будет выглядеть так:
#!/bin/sh
empty -f -i in.fifo -o out.fifo telnet -K localhost
empty -w -i out.fifo -o in.fifo -t 5 "ogin:" "luser"
empty -w -i out.fifo -o in.fifo -t 5 "assword:" "TopSecret"
empty -s -o in.fifo 'who am i > /tmp/test.txt'
empty -s -o in.fifo 'exit'
Чтож, это гораздо короче, чем кривоватый скрипт test_2.sh, да и
работает утилита вполне устойчиво на *BSD, Linux и Solaris не требуя
при этом наличия TCL, Perl или Python. Правда, функций в программе
пока меньше, чем у ее взрослых аналогов, но возможно, все еще впереди.
При запуске empty
telnet: Unable to connect to remote host: Connection refused
empty: Got nothing in output
я понимаю что хост не даступен, но как эту ошибку обойти?