Автоматизируем работу в интерактивных консольных программах используя 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 | #!/usr/bin/expect -f
spawn ftp example.com
expect {
"Name:" {
send "testuser\n"
expect "Password:"
send "testpass\n"
expect "ftp>"
send "passive\n"
}
}
interact
|
В первой строке мы записываем 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 | #!/usr/bin/expect -f
spawn ftp example.com
exp_internal 1
expect {
"*Name*" {
send "testuser\n"
expect "Password:"
send "testpass\n"
expect "ftp>"
send "passive\n"
}
}
interact
|
Выполним скрипт:
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 | #!/usr/bin/expect -f
if {[llength $argv] != 1} { # проверяем количество переданных аргументов скрипту
send_user "This script requires an argument: ftp host to connect"
exit 1 # если аргументов нет - завершаем выполнение скрипта с ошибкой
}
set HOST [lindex $argv 0] # в переменную HOST записываем первый аргумент - адрес хоста
spawn ftp $HOST # запускаем ftp и передаём аргумент $HOST
set timeout 3 # устанавливаем время ожидания expect в 3 секунды
set USERNAME "testuser" # имя пользователя и пароль так же вынесли в переменные для удобства
set PASSWORD "testpass"
expect {
"220 (vsFTPd ?????)" { # сейчас ожидаем строки с приветствием ftp сервера
expect "Name:"
send "$USERNAME\n"
expect "Password:"
send "$PASSWORD\n"
expect "ftp>"
send "passive\n"
} timeout { # выжидаем timeout
send_user "Unnable connect to $HOST"
exit 1
}
}
interact
|
Внимательный читатель наверняка заметил, что в приветствии 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 | #!/bin/bash
# Проверяем количество аргументов
if [ ! "$#" -eq 1 ]
then
echo "1 arguments required, $# provided"
exit 1
fi
# Проверяем доступность хоста
if ( ! ping -c1 -i1 -n -s10 -W1 $1 &>/dev/null )
then
echo "Host $1 not available"
exit 1
fi
USERNAME="testuser"
PASSWORD="testpass"
/usr/bin/expect<<EOF
spawn ftp $1
expect "Name*"
send "$USERNAME\n"
expect "Password:"
send "$PASSWORD\n"
expect "ftp>"
send "passive\n"
expect eof
EOF
|
Комментировать часть скрипта написанного на 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 | #!/bin/bash
if [ ! "$#" -eq 1 ]
then
echo "1 arguments required, $# provided"
exit 1
fi
if ( ! ping -c1 -i1 -n -s10 -W1 $1 &>/dev/null )
then
echo "Host $1 not available"
exit 1
fi
USERNAME="testuser"
PASSWORD="testpass"
DIRLIST=$(expect -c "
set timeout 3
spawn ftp $1
expect \"Name*\"
send \"$USERNAME\n\"
expect \"?assword:\"
send \"$PASSWORD\n\"
expect \"ftp>\"
send \"passive\n\"
expect \"ftp>\"
send \"ls\n\"
expect \"ftp>\"
send \"bye\n\"
")
echo "$DIRLIST" | sed -e '1,14d' | head -n -2 > $1_dirlist.txt
|
Пример принципиально не сильно отличается от предыдущего. В переменной 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 | #!/bin/bash
username="username"
password="password"
enpassword="enablepassword"
enpassword2="enablepassword2"
# IP-адрес берём из буфера обмена
ip=$(xsel -p)
re="^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$"
# Убираем всё ненужное от ip-адреса
ip=`echo $ip | sed -e 's/^[ \t]+|[ \t]+$//g'`
ip=`echo $ip | grep -Eo '([0-9]{1,3}\.){3}[0-9]{1,3}'`
# проверяем правильный ли ip-адрес это
if [[ ! $ip =~ $re ]] || [[ "${#ip}" -ge 16 ]] || [[ "${#ip}" -eq 0 ]]; then
notify-send "Telnet" "Error while extracting IP address"
-u normal
-i gtk-dialog-warning
-t 8000
exit 0;
fi
# проверяем доступность хоста
if ( ! fping -c1 -t500 $ip &>/dev/null ); then
notify-send "Telnet" "Host $ip not available. Check connection."
-u normal
-i gtk-dialog-warning
-t 8000
exit 1
fi
# В этой переменной храним собственно сам скрипт
commands="
spawn telnet $ip
expect {
# Cisco
\"User Access Verification\" {
send \"$username\n\"
expect \"Password: \"
send \"$password\n\"
send \"enable\n\"
send \"$enpassword\"
}
# Some other switch\router
\"Station's information:\" {
send \"$username\n\"
expect \"PassWord:\"
send \"$password\n\"
send \"enable\n\"
send \"$enpassword2\n\"
}
# and so on...
}
interact
"
gnome-terminal --geometry 120x30+30+20
--title "Telnet to $ip"
--execute /usr/bin/expect -c "$commands"
|
Доступность хоста проверяется с помощью fping, т.к. он позволяет задать меньший timeout ответа от хоста. После всех необходимых действий с входными данными, скрипт запускает gnome-terminal, в нём интерпретатор expect которому передаётся скрипт в виде переменной commands. В компании где я работаю, много разного оборудования управление которым осуществляется через telnet, поэтому в скрипте несколько блоков expect
- почти на каждый тип устройства. Здесь я оставил несколько.
Страница программы - http://expect.sourceforge.net. Там же есть небольшой список how-to и статей.