expect使用详解
工作中有一些交互式的操作,比如每个月更改openvpn的证书,如果用户过多我们就需要编写一个自动化的脚本来帮我们实现此功能,好比如我们想通过ssh的方式去控制其他的机器去执行一些操作,但是如果没有做秘钥认证呢,是的,expect这个工具很好,也很出名了,写篇博文记录一下。
一、expect概述
Expect是一个免费的编程工具语言,用来实现自动和交互式任务进行通信,而无需人的干预。Expect的作者Don Libes在1990年 开始编写Expect时对Expect做有如下定义:Expect是一个用来实现自动交互功能的软件套件 (Expect [is a] software suite for automating interactive tools)。使用它系统管理员的可以创建脚本用来实现对命令或程序提供输入,而这些命令和程序是期望从终端(terminal)得到输入,一般来说这些输入都需要手工输入进行的。 Expect则可以根据程序的提示模拟标准输入提供给程序需要的输入来实现交互程序执行。
Expect是不断发展的,随着时间的流逝,其功能越来越强大,已经成为系统管理员的的一个强大助手。Expect需要Tcl编程语言的支持,要在系统上运行Expect必须首先安装Tcl。Expect语言是基于Tcl的。Tcl实际上是一个子程序库,这些子程序库可以嵌入到程序里从而提供语言服务。最终的语言有点象一个典型的Shell语言。里面有给变量赋值的set命令,控制程序执行的if,for,continue等命令,还能进行普通的数学和字符串操作。
Expect是在Tcl基础上创建起来的,它还提供了一些Tcl所没有的命令。spawn命令激活一个Unix程序来进行交互式的运行。send命令向进程发送字符串。expect命令等待进程的某些字符串。expect支持正规表达式并能同时等待多个字符串,并对每一个字符串执行不同的操作。expect还能理解一些特殊情况,如超时和遇到文件尾。expect命令和Tcl的case命令的风格很相似。都是用一个字符串去匹配多个字符串。
博文来自:www.51niux.com
二、expect安装
2.1 yum安装:
# yum install expect -y #yum安装很简单一条命令就搞定了,这也是推荐的安装方式
部分安装过程:
Downloading Packages: (1/2): expect-5.44.1.15-5.el6_4.x86_64.rpm | 256 kB 00:00 (2/2): tcl-8.5.7-6.el6.x86_64.rpm | 1.9 MB 00:00 #会将tcl一起安装
2.2 编译安装:
安装tcl:
# wget http://prdownloads.sourceforge.net/tcl/tcl8.6.6-src.tar.gz #这就是官网跳转后的软件下载地址
# tar zxf tcl8.6.6-src.tar.gz
# cd tcl8.6.6/unix/
# ./configure --prefix=/usr/local/tcl && make && make install
安装expect:
# wget https://sourceforge.net/projects/expect/files/latest/download --no-check-certificate
# tar zxf expect5.45.tar.gz
# cd expect5.45
# ./configure --with-tclinclude=/usr/local/tcl/include/ --with-tclconfig=/usr/local/tcl/lib/ && make && make install
测试:
# expect
expect1.1> #回车 expect1.1> exit #退出
# expect -v
expect version 5.45
三、expect用法
expect从cmdfile中读取命令列表来执行,同样它也可以在有执行权限的脚本的第一行中加上#!标识来隐式地执行,如:
#!/usr/local/bin/expect -f
当然,路径应该准确地描述expect的位置,/usr/local/bin只是一个例子。
-c 参数
#指示其后的命令在脚本的最先开始执行,命令应该用引号引起来以不被shell打散。这个选项可被多次使用。多个命令如果用一个-c指示,则应用分号分隔。命令将按其书写顺序执行。(使用expectk时,这个参数用作-command)
例子:
命令:expect -c 'expect "\n" {send "pressed enter\n"}'
结果:pressed enter
#如果你执行了上面的脚本,它会等待输入换行符(\n)。按“enter”键以后,它会打印出“pressed enter”这个消息,然后退出。
-d 参数
#允许一些诊断输出,报告主要的expect和交互命令行为。在expect脚本开始用exp_internal 1也可以起到一样的作用,-d会多打出expect的版本。(strace命令在跟踪状态时很有用,trace命令在跟踪变量时很有用)(expectk中此参数为-diag)。
例子:# expect -d test.exp
#当你用“-d”选项执行代码的时候,你可以输出诊断的信息。
-D 参数
#打开交互debugger,后跟一个整数。如果这个整数是非零,或者^C被按下(或者碰到一个设置的断点,或者脚本中设置的其它合适的debugger命令)Debugger会在下一个tcl过程之前控制程序。关于debugger的信息参看README或SEE ALSO。(expectk中此参数为-Debug)
例子:
# expect -c 'set timeout 10' -D 1 -c 'set a 1'
1: set a 1
#"-D"选项左边的选项会在调试器启动以前被处理。然后,在调试器启动以后,剩下的命令才会被执行。
-f 参数
#指定从哪个文件中读取命令。当被用在#!指示(见上)中时此参数是可选的,所以其它参数可在命令行中提供。(expectk中为-file)。
-b 参数。
#缺省地,命令文件被整个地读到内存中执行,但是有时需要一行行地读取,比如,标准输入stdin就是这样。为了强制特定的文件被这样读入,可以使用-b参数。(expectk中为-buffer)。如果文件名是“-”,则表示从标准输入stdin读入。(用“./-”来表示一个叫作“-”的文件)
-i 参数
#使expect交互地提示输入命令,而不是从文件中读命令。命令提示行通过exit命令或一个eof字符结束。-i假设既没有命令文件,又没有使用-c参数。(expectk中为-interactive)。
例子:
# expect -i arg1 arg2 arg3
expect1.1> set argv
arg1 arg2 arg3
#当你执行上面的expect命令的时候(没有“-i”选项),它会把arg1当成脚本的文件名,所以“-i”选项可以让脚本把多个参数当成一个连续的列表。当你执行带有“-c”选项的expect脚本的时候,这个选项是十分有用的。因为默认情况下,expect是交互地执行的。
--
#用来对选项参数结束的划界。在你想传递一个象选项参数样的参数给你的脚本时,这个选项是很有用的,它使得expect不对其进行翻译。也可以放在#!行来阻止expect对任何选项参数格式的参数的翻译。比如,下面例子将保留原始参数(包括脚本名)到argv中:#!/usr/local/bin/expect 注意加参数到#!行时应该遵守getopt(3)和execve(2)的惯例。
-N 选项。
$exp_library/expect.rc文件如果存在的话将被自动的启用,除非-N选项被使用。(expectk中为-NORC)这样的话就会自动找~/.expect.rc,除非加了-n参数。如果定义了环境变量DOTDIR,那就会从那里找.expect.rc。(expectk中为-norc)。expect.rc的使用只在执行完-c参数指定的命令后。
-v
打印expect的版本号并退出。(expectk中为-version)
可选的args
#被结构化成一个列表存在argv中,argc被初始化成argv的长度。argv0被定义为脚本的名字。下面例子打印出脚本名和前三个参数:send_user "$argv0 [lrange $argv 0 2]
博文来自:www.51niux.com
四、expect脚本编写语法
expect的核心是spawn expect send set
spawn #调用要执行的命令 expect #等待命令提示信息的出现,也就是捕捉用户输入的提示: send #发送需要交互的值,替代了用户手动输入内容 set #设置变量值 interact #执行完成后保持交互状态,把控制权交给控制台,这个时候就可以手工操作了。如果没有这一句登录完成后会退出,而不是留在远程终端上。 expect eof #这个一定要加,与spawn对应表示捕获终端输出信息终止,类似于if....endif,expect脚本必须以interact或expect eof结束,执行自动化任务通常expect eof就够了。
#设置expect永不超时
set timeout -1
#设置expect 300秒超时,如果超过300没有expect内容出现,则退出
set timeout 300
expect编写语法,expect使用的是tcl语法。
一条Tcl命令由空格分割的单词组成. 其中, 第一个单词是命令名称, 其余的是命令参数cmd arg arg arg $符号代表变量的值. 在本例中, 变量名称是foo。如$foo 方括号执行了一个嵌套命令. 例如, 如果你想传递一个命令的结果作为另外一个命令的参数, 那么你使用这个符号[cmd arg] 双引号把词组标记为命令的一个参数. "$"符号和方括号在双引号内仍被解释"some stuff" 大括号也把词组标记为命令的一个参数. 但是, 其他符号在大括号内不被解释{some stuff} 反斜线符号是用来引用特殊符号. 例如:n 代表换行. 反斜线符号也被用来关闭"$"符号, 引号,方括号和大括号的特殊含义
博文来自:www.51niux.com
五、expect实例脚本
5.1:登录对端服务器并执行命令
# vim 01.exp #这是我们的expect脚本名称以.exp结尾
#!/usr/bin/expect set timeout 30 spawn ssh root@192.168.50.140 "hostname" expect "password:" send "123456\r" expect eof
#!/usr/bin/expect #跟shell一样,这里定义了expect的命令的绝对路径
set timeout 30 #超时时间是多少,单位是秒,默认是10秒,timeout后面还可以跟提示信息,下面例子会写。
spawn ssh root@192.168.50.140 "hostname" #spawn命令是要执行的操作,我们这里就是登陆服务器并运行一条命令"hostname"
expect "password:" #expect 等待一个匹配的流出流中的内容,如右图:
send "123456\r" #send是匹配到相关的字符之后向输入流写入的内容,也就是我们所说的交互式需要输入的内容,123456正是我对端服务器的密码,\r是回车
expect eof #表示读取到文件结束符。
下面是执行结果:# expect 02.exp
spawn ssh root@192.168.50.140 hostname root@192.168.50.140's password: client
#直接把我们的hostname输出了
还是接着上面的例子,有一个参数就在这里说一下,你看现在我们是让对端服务器执行完命令就完事了,如果想直接就登陆对端服务器手工交互执行命令呢?
# vim 01.exp
#!/usr/bin/expect set timeout 30 spawn ssh root@192.168.50.140 #这里的执行命令去掉 expect "password:" send "123456\r" #expect eof #结束符不要 interact #执行完成后保持交互状态,把控制权交给控制台,这个时候就可以手工操作了。如果没有这一句登录完成后会退出,而不是留在远程终端上。
下面是部分执行结果截取:
# expect 01.exp
spawn ssh root@192.168.50.140 root@192.168.50.140's password: Last login: Fri Aug 5 00:57:09 2016 from 192.168.50.129 [root@client ~]# ifconfig eth0 Link encap:Ethernet HWaddr 00:0C:29:9E:0B:5D inet addr:192.168.50.140 Bcast:192.168.50.255 Mask:255.255.255.0
# hostname #手工执行命令
client
[root@client ~]# ^C #如果要退出CRTL+C时没用的,因为已经在对端的机器了
[root@client ~]# exit #exit退出当前会话
logout Connection to 192.168.50.140 closed.
这里还有一个问题关于ssh登录执行命令的脚本还有一个问题,就是我们很多机器可能是第一次连接会话,这是需要提示yes/no操作的,所以应该完善一下。
#!/usr/bin/expect
set timeout 3 spawn ssh root@192.168.50.140 expect { #因为我有多段的send要发送所以用{}包一下 "*yes/no" { send "yes\r";exp_continue} #这里第一个要发送的判断如果让输入yes/no,发送yes,并且我们还有一段呢,所以用exp_continue使在执行完当前的动作之后,继续执行模式匹配,exp_continue命令的使用方法,首先它要处于一个expect命令中,然后它属于一种动作命令完成的工作就是从头开始遍历, #也就是说如果没有这个命令,匹配第一个关键字以后就会继续匹配第二关键字,但有了这个命令后,匹配第一个关键字以后,第二次匹配依然从第一个关键字开始。 "*assword" {send "123456\r"} #这里就是发送密码了,字段前面加*好一点,*就是匹配多个的意思嘛。 } expect eof #执行完结束
执行结果:
# expect 01.exp
spawn ssh root@192.168.50.140 The authenticity of host '192.168.50.140 (192.168.50.140)' can't be established. RSA key fingerprint is c5:4a:74:fa:4e:16:1a:45:28:cf:4c:86:20:a8:fb:8f. Are you sure you want to continue connecting (yes/no)? yes #看这里是第一个判断输入 Warning: Permanently added '192.168.50.140' (RSA) to the list of known hosts. root@192.168.50.140's password: #这里是第二个判断输入 Last login: Fri Aug 5 01:22:53 2016 from 192.168.50.129 #登录成功 [root@client ~]# [root@localhost expect]# #因为我前面定义了timeout为3秒,所以登录三秒迅速的退回去了。
5.2:shell脚本嵌套expect脚本外加传参
说明:这里还是用一个比较常用的例子,批量分发密钥实现ssh无密码登陆,需要一个ssh脚本来实现传参,需要一个expect脚本来实现密码ssh登录。
(1). bash脚本叫做:key_send.sh
# cat key_send.sh #查看一下我们的bash脚本
#!/bin/bash username=root #这里定义了我们要通过秘钥的用户,等待传参。 for num in `cat /root/iplist` #这里写一个for循环,我们的iplist写了我们的主机IP:192.168.1.111和192.168.1.112 do expect /root/expect/02.exp $username $num #这里在循环里面执行我们的exp程序,并且传参了用户和ip变量 done
(2). exp的名称为02.exp,当然可以写得有意义点,此脚本用来进行公钥的传送
# cat 02.exp
#!/usr/bin/expect set timeout 10 set username [lindex $argv 0] #这里定义了第一个变量名以及参数所在的位置,0 就为第一个位置。 set hostip [lindex $argv 1] #这里定义了一个主机变量,参数所在的位置为传参的第二个位置。 spawn ssh-copy-id -i /root/.ssh/id_rsa.pub $username@$hostip #这里定义了我们要执行的命令以及对应的变量 expect { "*yes/no" {send "yes\r";exp_continue} "*password:" {send "654321\r"} } expect eof
执行结果:
# sh key_send.sh
spawn ssh-copy-id -i /root/.ssh/id_rsa.pub root@192.168.1.111 root@192.168.1.111's password: Now try logging into the machine, with "ssh 'root@192.168.1.111'", and check in: .ssh/authorized_keys to make sure we haven't added extra keys that you weren't expecting. spawn ssh-copy-id -i /root/.ssh/id_rsa.pub root@192.168.1.112 root@192.168.1.112's password: Now try logging into the machine, with "ssh 'root@192.168.1.112'", and check in: .ssh/authorized_keys to make sure we haven't added extra keys that you weren't expecting.
登录测试:
# ssh 192.168.1.111
Last login: Mon Aug 8 23:41:28 2016 from master.hadoop
[root@slave1 ~]# exit
logout Connection to 192.168.1.111 closed.
[root@master expect]# ssh 192.168.1.112
Last login: Mon Aug 8 23:41:33 2016 from master
#上述结果显示实现了无密码的密钥登录。
5.3:expect脚本内部定义参数并传参
上一个例子讲述的是expect脚本被sh脚本包含,并将参数传递给expect脚本,这个例子讲的是expect自己内部制定变量并传参。
# cat 03.exp
#!/usr/bin/expect #if { $argc < 4 } { #这里运用了if语句,这个$argc表示参数的数目,这里判断参数数目小于4就执行下面的语句,$argv0为脚本名字本身,$argv为命令行参数。[lrange$argv 0 0]表示第1个参数,[lrange $argv 0 4]为第一个到第五个参数。 if { [llength $argv] < 4 } { #这里跟上面的意思一致,这里是计算参数的长度,当它的长度小于4就执行下面的语句 puts "Usage:$argc <host> <username> <password> <cmd>" #puts可以读取变量,输出内容,跟下面一句话的意思一致,所以这里贴了两句,这里意思是打印输入参数的个数,然后<host> <username> <password> <cmd>输出。 send_user "Usage:$argv0 cmd <host> <username> <password> <cmd>\n" #send_user 命令用来把后面的参数输出到标准输出中去,默认的的send、exp_send 命令都是将参数输出到程序中去的。这里的$argv0是脚本本身,send_user默认不换行,所以要加\n换行符。 exit 1 #这里是打印一个状态码,好让后面的程序知道一个返回的结果。 } set timeout 3 set host [lindex $argv 0] #定义host变量的位置,set 就是设置变量名。 set username [lindex $argv 1] #用户名在第二个参数位置 set password [lindex $argv 2] #密码在第三个参数位置,也可以set password 654321 这样直接指定变量以及实际密码。 set cmd [lindex $argv 3] #执行命令在第四个参数位置 spawn ssh $username@$host $cmd #执行语句,并传参 expect { "yes/no" {send "yes\r";exp_continue} "*password" {send "$password\r"} #密码传参到了这里 } expect eof
执行结果:
先来一个错误的结果示例:
# expect 03.exp 192.168.1.111 root 654321
Usage:3 <host> <username> <password> <cmd> Usage:03.exp cmd <host> <username> <password> <cmd>
[root@master expect]# echo $?
1
再来一个正确的结果示例:
# expect 03.exp 192.168.1.111 root 654321 ifconfig
spawn ssh root@192.168.1.111 ifconfig root@192.168.1.111's password: eth0 Link encap:Ethernet HWaddr 00:0C:29:FA:C9:A2 inet addr:192.168.1.111 Bcast:192.168.1.255 Mask:255.255.255.0
5.4:openvpn生成用户证书
# cat /root/expect/vpnkey.sh
#!/bin/bash easy_dir=/etc/openvpn/easy-rsa/2.0 key_pwd=$easy_dir/keys #create vpn server file cd $easy_dir source vars #假设我们之前的CA,server证书已经创建完毕了,如果某天有新的用户需要创建用户证书了,如果./vars就需要./clean-all、CA证书、server证书重新创建一遍,还要把以前发过的证书重新发一次, #当然如果你确实想什么都更换的话,如每月全部更换一次证书的话,可以执行./vars,但是如果你只是想添加普通用户的openvpn的登录证书呢,就用source vars这种形式。 expect /root/expect/vpnkey.exp $1 #这里是调用一个vpnkey.exp来帮助我们完成生成普通用户的操作,$1就是用到了shell的位置传参。
# cat /root/expect/vpnkey.exp #看下调用的exp脚本,我这里写的比较简单了。
#!/usr/bin/expect
set user [lindex $argv 0] #这里定义了添加用户所在的位置,第一个参数位置 set timeout 1 spawn /etc/openvpn/easy-rsa/2.0/build-key $user #这里是生成哪个普通用户的vpn证书,$user 在第一位,就接受了来着shell脚本$1传递过来的信息。 expect "]:" send "\r" expect "]:" send "\r" expect "]:" send "\r" expect "]:" send "\r" expect "]:" send "\r" expect "]:" send "\r" expect "]:" send "\r" expect "]:" send "\r" expect "]:" send "\r" expect "]:" send "\r" expect "y/n]:" send "y\r" expect "y/n]:" send "y\r" expect eof
执行结果:
# sh /root/expect/vpnkey.sh chenggong #这里就是调用脚本,将chenggong的用户传递给脚本,脚本就会去生成一个chenggong的用户
# ls -l /etc/openvpn/easy-rsa/2.0/keys/chenggong.* #上面的执行过程就不粘贴了,是成功的,这里用户成功证书成功创建。
-rw-r--r-- 1 root root 4006 8月 9 14:20 /etc/openvpn/easy-rsa/2.0/keys/chenggong.crt -rw-r--r-- 1 root root 733 8月 9 14:20 /etc/openvpn/easy-rsa/2.0/keys/chenggong.csr -rw------- 1 root root 916 8月 9 14:20 /etc/openvpn/easy-rsa/2.0/keys/chenggong.key
博文来自:www.51niux.com
5.5:scp文件到对端服务器
# cat scp.exp
#!/usr/bin/expect set timeout 10 set host [lindex $argv 0] set username [lindex $argv 1] set password [lindex $argv 2] set src_file [lindex $argv 3] set dest_file [lindex $argv 4] spawn scp $src_file $username@$host:$dest_file expect { "(yes/no)?" { send "yes\n" expect "*assword:" { send "$password\n"} } "*assword:" { send "$password\n" } } expect "100%" expect eof
#这里有一点要注意的就是,当密码带有特殊字符的时候需要转义一下。
执行结果:
[root@localhost expect]# expect scp.exp 192.168.50.140 root 123!@#321 /root/expect/vpnkey.exp /opt/ #第一个出错的例子就是密码有特殊字符没有转义
-bash: !@#321: event not found
[root@localhost expect]# expect scp.exp 192.168.50.140 root 123\!\@\#321 /root/expect/vpnkey.exp /opt/ #第二个例子密码进行了转义,scp文件成功。
spawn scp /root/expect/vpnkey.exp root@192.168.50.140:/opt/ root@192.168.50.140's password: vpnkey.exp
#如果要发送多个文件或者涉及到多变量,上面也有例子提及到,就嵌套到shell脚本里来执行。
5.6:登录对端服务器并执行多段命令
# vim 06.exp
#!/usr/bin/expect -f set ip 192.168.50.140 set password [lindex $argv 0] set timeout 10 spawn ssh root@$ip expect { "*yes/no" { send "yes\r"; exp_continue} "*password:" { send "$password\r" } } expect "*# " send "pwd\r" expect "*# " send "mkdir /tmp/test \r" expect "*# " send "touch /tmp/test/nihao \r" send "exit\r" expect eof
#这个脚本就是通过密码传参,登录到对端的机器上面,执行三个命令,pwd,mkdir /tmp/test,touch /tmp/test/nihao,然后退出对端服务器,然后结束。这种方式我觉得是有意义的,当我们没有用到puppet+mco又或者ansible进行集群管理,只能通过密码管理的话,如果让所有的机器都去下载某个脚本然后并运行,就需要登录每一台机器去操作。而这种方式呢,可以还是脚本嵌套的形式,让服务器执行多段的命令来完成我们预期的操作。
至此。关于expect的操作就写到这里,当然expect还有很多高级的用法,像循环while for,加参数-re运用正则表达式等,python也可以调用expect,大家有兴趣可以去了解。