Автоматизируем работу в интерактивных консольных программах используя expect
В жизни сетевого инженера (да и не только), наступает такой момент, когда некоторые рутинные операции надоедает выполнять, и хочется их оптимизировать. В один прекрасный день я понял, что каждый раз когда мне нужно авторизоваться на коммутаторе, то набирать логин\пароль, а затем ещё пароль на enable мне надоело. Поэтому данное действие было решено как-то оптимизировать. Взяв бутылочку пенного я сел за "работу"..
В компании где я работаю, по определённым обстоятельствам авторизация на коммутаторах\роутерах\DSLAM и прочем оборудовании происходит не по ssh, а по telnet. Средствами самого telnet возможности передать логин\пароль нет, поэтому поиск с попутным распитием алкоголя продолжился, и остановился на утилите expect.
Expect - это утилита, которая парсит потоковый вывод консольных программ, и в ответ на них отправляет какой либо заранее предусмотренный "ответ". Например, при подключении к ftp серверу, ожидаем получить запрос на ввод пароля, и при его получении - отправляем его.
Для моей задачи expect подошёл идеально. Да и как оказалось, у одного из коллег уже был небольшой expect скрипт для этих целей, который, правда, не совсем подходил мне, но для ознакомления с expect пришёлся весьма кстати.
Чтобы более или менее понять, рассмотрим для начала небольшой expect скрипт, который авторизует пользователя на ftp сервере:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
В первой строке мы записываем sha-bang, которым указывает командной
оболочке, что этот скрипт следует интерпретировать как expect-script
(запустить expect и передать ему скрипт). Затем используя spawn
вызываем программу ftp и передаём ей в качестве аргумента адрес хоста, к
которому хотим подключиться. И вот самое интересное - блок expect {}
.
В нём мы сообщаем интерпретатору, что ожидаем "Name: ", и в ответ с
помощью send
отправляем логин и символ перевода каретки \n
(Enter).
Затем ожидаем получить запрос на ввод пароля, отправляем пароль и
переводим ftp-клиент в пассивный режим (send "passive\n"
). В самом
конце у нас interact
который указывает, что необходимо по завершении
сценария передать управление пользователю.
Если сильно раздражает вывод программ во время выполнения скрипта, то
можно отключить это добавив log_user 0
.
Если что-то во время выполнения скрипта идёт не так, то можно посмотреть
более подробно, какие данные получает expect, нашёл ли совпадения, и что
посылает в ответ. Для этого надо добавить exp_internal 1
в код
скрипта:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Выполним скрипт:
spawn ftp example.com
expect: does "" (spawn_id exp6) match glob pattern "*Name*"? no
Connected to example.com.
expect: does "Connected to example.com.\r\n" (spawn_id exp6) match glob pattern "*Name*"? no
220 (vsFTPd 2.3.2)
Name (example.com:testuser):
expect: does "Connected to example.com.\r\n220 (vsFTPd 2.3.2)\r\nName (example.com:testuser): " (spawn_id exp6) match glob pattern "*Name*"? yes
expect: set expect_out(0,string) "Connected to example.com.\r\n220 (vsFTPd 2.3.2)\r\nName (example.com:testuser): "
expect: set expect_out(spawn_id) "exp6"
expect: set expect_out(buffer) "Connected to example.com.\r\n220 (vsFTPd 2.3.2)\r\nName (example.com:testuser): "
send: sending "testuser\n" to { exp6 }
expect: does "" (spawn_id exp6) match glob pattern "Password:"? no
testuser
expect: does "testuser\r\n" (spawn_id exp6) match glob pattern "Password:"? no
331 Please specify the password.
Password:
expect: does "testuser\r\n331 Please specify the password.\r\nPassword:" (spawn_id exp6) match glob pattern "Password:"? yes
expect: set expect_out(0,string) "Password:"
expect: set expect_out(spawn_id) "exp6"
expect: set expect_out(buffer) "testuser\r\n331 Please specify the password.\r\nPassword:"
send: sending "testpass\n" to { exp6 }
expect: does "" (spawn_id exp6) match glob pattern "ftp>"? no
expect: does "\r\n" (spawn_id exp6) match glob pattern "ftp>"? no
530 Login incorrect.
Login failed.
expect: does "\r\n530 Login incorrect.\r\nLogin failed.\r\n" (spawn_id exp6) match glob pattern "ftp>"? no
ftp>
expect: does "\r\n530 Login incorrect.\r\nLogin failed.\r\nftp> " (spawn_id exp6) match glob pattern "ftp>"? yes
expect: set expect_out(0,string) "ftp>"
expect: set expect_out(spawn_id) "exp6"
expect: set expect_out(buffer) "\r\n530 Login incorrect.\r\nLogin failed.\r\nftp>"
send: sending "passive\n" to { exp6 }
tty_raw_noecho: was raw = 0 echo = 1
spawn id exp6 sent >
passive
Passive mode on.
ftp>
Видно, что после отправки пароля, ftp сервер снова его спрашивает. Значит, ошиблись где-то в логине или пароле.
Теперь пример сложнее. Скрипт выше делает то, что нам нужно в данном
случае, но в нём есть некоторые недостатки: для каждого хоста необходимо
создавать новый скрипт; если сайт недоступен, то скрипт будет вести себя
весьма странно.
Исправим эти недостатки немного модифицировав его (для удобства
добавленные\изменённые участки кода прокомментированы):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
Внимательный читатель наверняка заметил, что в приветствии ftp сервера
вместо версии - вопросительные знаки. Дело в том, что expect может
проверять соответствия используя регулярные выражения, или
(по-умолчанию) на основе wildcards. В данном случае неизвестные цифры
версии ftp сервера мы заменили на "?".
Далее вместо строки для поиска совпадений мы указываем интерпретатору,
что если совпадений не нашли, ждём определённый промежуток времени
указанный в set timeout
, выводим сообщение об ошибке и завершаем
выполнение скрипта.
В принципе, на этом стоит закончить с данным примером, но можно
добавить ещё один интересный штрих: убрать лишний вывод. В данном
примере, если expect не нашёл совпадений, наше сообщение об ошибке будет
трудно заметить на фоне остального вывода. Отключить его можно добавив
log_user 0
. В любом нужном месте его можно включить заменив 0 на 1.
Всё что отправлено с помощью send_user
будет по-прежнему выводиться.
Всё бы хорошо, но всё ещё чего-то не хватает скрипту, что-то не так... И
правда: проверка таким образом доступности ftp сервера не самая лучшая
идея.
Хороший выход из данной ситуации - это нужную нам часть вынести в bash
скрипт, проверку доступности выполнять обычным пингом, а количество
аргументов средствами самого bash:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
Комментировать часть скрипта написанного на bash я не буду - для этого
есть ресурсы лучше. Какой либо особой магии здесь нет. Просто проверяем
количество аргументов переданных скрипту, доступность сервера для
подключения и если всё в порядке - запускаем expect передавая ему
команды. expect eof
здесь нужен чтобы скрипт не завершался сразу после
выполнения.
Раз уж мы заговорили про использование expect в bash, то коснёмся и сбора данных. Для примера давайте представим, что есть сферический ftp сервер в вакууме, с которого зачем-то нужно раз в сутки собирать список имеющихся директорий. Ситуация надуманная, но для примера подойдёт:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
|
Пример принципиально не сильно отличается от предыдущего. В переменной
DIRLIST
запускаем expect и построчно выполняем скрипт. Следует
обратить внимание, что так как мы запустили expect внутри bash, то надо
дополнительно экранировать посылаемые и ожидаемые данные. Далее работаем
с полученными результатами как с простым текстом.
Для моих нужд в результате получился такой скрипт:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
|
Доступность хоста проверяется с помощью fping, т.к. он позволяет задать
меньший timeout ответа от хоста. После всех необходимых действий с
входными данными, скрипт запускает gnome-terminal, в нём интерпретатор
expect которому передаётся скрипт в виде переменной commands. В компании
где я работаю, много разного оборудования управление которым
осуществляется через telnet, поэтому в скрипте несколько блоков
expect
- почти на каждый тип устройства. Здесь я оставил несколько.
Страница программы - http://expect.sourceforge.net. Там же есть небольшой список how-to и статей.