本文发布于Cylon的收藏册,转载请著名原文链接~

什么是Expect

Expent是基于tcl的相对简单的一个免费的脚本编程工具语言,用来实现自动和交互任务程序进行通信,无需人的手工干预。比如SSH、FTP等,这些程序正常情况都需要手工与它们进行交互,而使用Expect就可以模拟人手工交互的过程,实现自动的和远程的程序交互,从而达到自动化运维的目的。

Expect程序工作流程

Expect的工作流程可以理解为,spawn启动进程 ==> expect期待关键字 ==> send向进程发送字符 ==> 退出结束。

安装Expect软件

首先,配置好yum安装源,并且确保机器可以上网,然后执行yum install expect -y即可安装Expect软件。

安装完后查看结果:

1
2
$ rpm -qa|grep expect
expect-5.44.1.15-5.el6_4.x86_64

先看一个Expect小实例

首先准备3台虚拟机:

1
2
3
4
192.168.252.60 client
192.168.252.62 client
192.168.252.63 client
192.168.252.64 server

再执行下面例子前,我们先手工执行如下命令

1
ssh -p52113 root@192.168.252.64 ifconfig

执行结果:

1
2
$ ssh -p52113 root@192.168.252.64 ifconfig
root@192.168.252.64's password: 

Expect 例子脚本内容

1
2
3
4
5
6
7
#!/usr/bin/expect
spawn ssh -p52113 root@192.168.252.62 /sbin/ifconfig eth0
set timeout 60
expect "*password:"
send "111111\n"
expect eof
exit

执行结果:问题:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ ./a.exp  
spawn ssh -p52113 root@192.168.252.62 /sbin/ifconfig eth0
root@192.168.252.62's password: 
eth0      Link encap:Ethernet  HWaddr 00:0C:29:C3:48:CB  
          inet addr:192.168.252.62  Bcast:192.168.252.255  Mask:255.255.255.0
          inet6 addr: fe80::20c:29ff:fec3:48cb/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:3027 errors:0 dropped:0 overruns:0 frame:0
          TX packets:1632 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:268416 (262.1 KiB)  TX bytes:175473 (171.3 KiB)

问题

1
2
3
4
5
$ ./a.exp 
./a.exp: line 8: spawn: command not found
couldn't read file "*password:": no such file or directory
./a.exp: line 11: send: command not found
couldn't read file "eof": no such file or directory

原因:

  • 含有expect的脚本不能用bash执行,bash无法解析。添加可执行权限后,直接./your_script即可
  • 这个代码是expect的代码,不由bash解释。
  • spawn可以看作expect脚本的内部函数。要第一行改成 #!/usr/bin/expect

Expect语法

Expect中的命令是最重要的部分了,命令的使用语法如下:

1
命令 [选项] 参数

spawn

spawn命令是Expect的初始命令,它用于启动一个进程,之后所有expect操作都在这个进程中进行,如果没有spawn语句,整个expect就无法执行了,spawn使用方法如下:

1
spawn ssh root@192.168.252.62

在spawn命令后面直接加上要启动的进程、命令等信息,除此之外,spawn还支持其他选项如:

  • -open 启动文件进程,具体说明省略。
  • -ignore 忽略某些信号,具体说明省略。

expect

使用方法:

1
expect  表达式  动作  表达式  动作

expect命令用于等候一个相匹配内容的输出,一旦匹配上就执行expect后面的动作或命令。这个命令接受几个特有的参数,用的最多的就是-re,表示使用正则表达式的方式匹配,使用起来就像这样:

1
2
spawn  ssh  root@192.168.252.64
expect  "password:" {send "111111\r"}

从上面的例子可以看出,expect是依附与spawn命令的,当执行ssh命令后,expect就匹配命令执行后的关键字:“password:”,如果匹配到关键字就会执行后面包含在“{}”中的send或exp_send动作,匹配的动作可以放在二行,这样就不需要使用“{}”了,就像下面这样,实际完成的功能与上面是一样的:

1
2
3
spawn  ssh  root@192.168.252.64
expect  "password:" 
send "111111\r"

expect命令还有一种用法,他可以在一个expect匹配中多次匹配关键字,并给出处理动作,只需要将关键字放在一个大括号中就可以了,当然还要有exp_continue。

1
2
3
4
5
spawn ssh root@192.168.252.64
expect {
    "yes/no"		{ exp_send "yes\r"; exp_continue }
   "*password:"	{ exp_send "111111\r" }
}

exp_send和send

在上面的介绍中,我们已经看到exp_send命令的使用,exp_send命令是expect中的动作命令,它还有一个完成同样工作的同胞:send,exp_send命令可以发送一些特殊符号,我们看到了\r(回车),还有一些其他的比如:\n、\t等等,这些都与TCL中的特殊符号相同。

1
2
3
4
5
spawn ssh root@192.168.252.64
expect {
  "yes/no"		{ exp_send "yes\r"; exp_continue }
  "*password:"	{ exp_send "111111\r" }
}

send命令有几个可用参数:

  • -i 制定spawn_id,这个参数用来向不同spawn_id的进程发送命令,是进行多程序控制的关键参数。
  • -s s代表slowly,也就是控制发送的速度,这个参数使用的时候要与expect中的变量send_slow相关联。

exp_continue

这个命令一般用在动作中,它被使用的条件比较苛刻,看看下面的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#!/usr/bin/expect
spawn ssh root@192.168.252.64 /sbin/ifconfig eth0
set timeout 60
expect {
    -timeout 1
    "yes/no"		{ exp_send "yes\r"; exp_continue }
    "*password:"	{ exp_send "111111\r" }
    timeout 		{ puts  "expect was timeout by oldboy.";  return}
}
expect eof
exit

在这个例子中,可以发现exp_continue命令的使用方法,首先它要处于一个expect命令中,然后它属于一种动作命令,完成的工作就是从头开始遍历,也就是说如果没有这个命令,匹配第一个关键字以后就会继续匹配第二个关键字,但有了这个命令后,匹配第一个关键字以后,第二次匹配仍然从第一个关键字开始。

send_user

send_user命令用来把后面的参数输出到标准输出中去,默认的send、exp_send命令都是将参数输出到程序中去的,用起来就像这样:

1
send_user "please input password:"

这个语句就可以在标准输出中打印Please input password:字符了。

 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
if { $argc != 2 } {
send_user "usage: expect scp-expect.exp file host dir\n"
exit
}

#define var
set file [lindex $argv 0]
set host [lindex $argv 1]
set password "111111"
#spawn scp /etc/hosts root@10.0.0.142:/etc/hosts
spawn ssh-copy-id -i $file "-p 52113 root@$host"

expect {
    "yes/no"    {send "yes\r";exp_continue}
    "*password" {send "$password\r"}
}

expect eof

exit -onexit {
send_user "Oldboy say good bye to you!\n"
}

#script usage
#expect oldboy-6.exp file host dir
#example
#./oldboy-6.exp /etc/hosts 10.0.0.179 /etc/hosts

exit

exit命令功能很简单,就是直接退出脚本,但是你可以利用这个命令对脚本做一些扫尾工作,比如下面这样:

1
2
3
4
exit -onexit {
    exec  rm  $tmpfile
    send_user "Good bye\n"
}
 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
if { $argc != 2 } {
	send_user "usage: expect scp-expect.exp file host dir\n"
	exit
}

#define var
set file [lindex $argv 0]
set host [lindex $argv 1]
set password "111111"
#spawn scp /etc/hosts root@10.0.0.142:/etc/hosts
spawn ssh-copy-id -i $file "-p 52113 root@$host"

expect {
 	"yes/no"    {send "yes\r";exp_continue}
 	"*password" {send "$password\r"}
}

expect eof

exit -onexit {
 	send_user "Oldboy say good bye to you!\n"
}

#script usage
#expect oldboy-6.exp file host dir
#example
#./oldboy-6.exp /etc/hosts 10.0.0.179 /etc/hosts

Except变量

expect中有很多有用的变量,它们使用方法与TCL语言中的变量相同,比如: set 变量名 变量名 # 设置变量的方法 set $变量名 # 读取变量的方法

1
2
3
4
5
#define var
set file [lindex $argv 0]
set file [lindex $argv 1]
set file [lindex $argv 2]
set password "123456"

Expect关键字

expect中的特殊关键字用于匹配过程,代表某些特殊含义或状态,一般用于expect族命令中不能在外面单独使用,也可以理解为事件,使用上类似于expect eof {}

eof

eof (end-of-file)关键字用于匹配 结束符,比如文件的结束符、FTP传输停止等情况,在这个关键字后跟上动作来做进一步的控制,特别是FTP交互操作方面,它的动作很大。用一个例子来看看:

1
2
3
4
5
6
spawn ftp anonymous@10.11.105.110
expect {
  "password:"  {exp_send "who I’m I"}
   eof {ftp connect close}
}
interact { }

timeout

timeout是expect中的一个重要变量,它是一个全局性的时间控制开关,你可以通过为这个变量赋值来规定整个expect操作时间,注意这个变量是服务于expect全局的,它不会纠缠于某一条命令,即使命令没有任何错误,到时见仍然会激活这个变量,但这个时间到达以后除了激活一个开关之外不会做其他的事情,如何处理是脚本编写人员的事情,看看它的实际使用方法:

1
2
3
4
set timeout 60
spawn ssh root@192.168.2.1
expect "password:"  {send "word\r"}
expect timeout  { puts "Expect was timeout"; return }

上面的处理中,首先将timeout变量设置为60秒,当出现问题的时候程序可能会停止下来,只要到60秒。就会激活下面的timeout动作,这里我们打印一个信息并且停止了脚步的运行–你可以做更多其他的事情,看自己的意思。

在另一种expect格式中,我们还有一种设置timeout变量的方法,看看下面的例子:

1
2
3
4
5
6
7
8
9
spawn ssh root@192.168.1.1
expect {
    -timeout 60
    -re "password:" {exp_send "word\r"}
    -re "TopsecOS#" {  }
    timeout { puts "Expect was timeout"; return }
    -re "TopsecOS#" {  }
    timeout  { puts "Expecr was timeout"; return }
}

生产场景Expect实战

 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
$ cat fenfa.exp
#!/usr/bin/expect
if { $argc != 3 } {
         send_user "usage: expect scp-expect.exp file host dir\n"
         exit
}

#define var
set file [lindex $argv 0]
set host [lindex $argv 1]
set dir  [lindex $argv 2]
set password "111111"
spawn scp -P 52113 -p $file root@$host:$dir
#spawn ssh-copy-id -i $file "-p 52113 root@$host"

expect {
    "yes/no"    {send "yes\r";exp_continue}
    "*password" {send "$password\r"}
}

expect eof

exit -onexit {
    send_user "Oldboy say good bye to you!\n"
}

#script usage
#expect oldboy-6.exp file host dir
#example
#./oldboy-6.exp /etc/hosts 10.0.0.179 /etc/hosts

实战1:使用expect实现批量分发/etc/hosts文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ cat hosts.sh 
#!/bin/sh
. /etc/init.d/functions
for ip in 192.168.252.60 192.168.252.64  192.168.252.62
do
expect fenfa.exp /etc/hosts/ $ip ~/ >/dev/null 2>&1
if [ $? -eq 0 ];then
    action "$ip" /bin/true
else 
    action "$ip" /bin/false
fi
done

如果ip很多写循环列表

1
for ip in `cat ip_list`

测试结果:

1
2
3
4
$ sh hosts.sh   
192.168.252.60                                             [确定]
192.168.252.64                                             [确定]
192.168.252.62                                             [确定]

实战2:使用expect批量分发SSH密钥文件

首先准备4台机器:

IP hostname-
192.168.252.60 lamp_client
192.168.252.62 rsync_client
192.168.252.63 nfs_server
192.168.252.64 mysql_client

1 在server端创建公钥与私钥

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ ssh-keygen -t dsa
Generating public/private dsa key pair.
Enter file in which to save the key (/root/.ssh/id_dsa): 
Created directory '/root/.ssh'.
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /root/.ssh/id_dsa.
Your public key has been saved in /root/.ssh/id_dsa.pub.
The key fingerprint is:
85:c5:f0:88:31:ed:a6:e6:9f:54:61:1e:68:ee:b2:c7 root@server-nfs
The key's randomart image is:
+--[ DSA 1024]----+
|      o..o.      |
|       +.*.      |
|      ..= *      |
|       oo+ o     |
|       oS o      |
|      o. .       |
|     o..o        |
|      .+E.       |
|      .oo        |
+-----------------+

2 查看密钥

1
2
3
$ ll ~/.ssh/
id_dsa
id_dsa.pub

3 写批量拷贝脚本

 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
#!/usr/bin/expect
if { $argc != 2 } {
    send_user "usage: expect scp-expect.exp file host dir\n"
    exit
}
#define var
set file [lindex $argv 0]
set host [lindex $argv 1]
set password "111111"
#spawn scp /etc/hosts root@10.0.0.142:/etc/hosts
spawn ssh-copy-id -i $file "-p 52113 root@$host"
expect {
     "yes/no"    {send "yes\r";exp_continue}
     "*password" {send "$password\r"}
}

expect eof

exit -onexit {
     send_user "Oldboy say good bye to you!\n"
}

#script usage
#expect oldboy-6.exp file host dir
#example
#./oldboy-6.exp /etc/hosts 10.0.0.179 /etc/hosts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ ll ~/.ssh/
authorized_keys

$ ll ~/.ssh/
id_dsa
id_dsa.pub
known_hosts

$ ll ~/.ssh/
authorized_keys

$ ll ~/.ssh/
authorized_keys
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
$ ssh -p52113 root@192.168.252.60 ifconfig
eth0      Link encap:Ethernet  HWaddr 00:0C:29:A7:DB:43  
          inet addr:192.168.252.60  Bcast:192.168.252.255  Mask:255.255.255.0
          inet6 addr: fe80::20c:29ff:fea7:db43/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:1187 errors:0 dropped:0 overruns:0 frame:0
          TX packets:792 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:105294 (102.8 KiB)  TX bytes:73135 (71.4 KiB)

lo        Link encap:Local Loopback  
          inet addr:127.0.0.1  Mask:255.0.0.0
          inet6 addr: ::1/128 Scope:Host
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:40 errors:0 dropped:0 overruns:0 frame:0
          TX packets:40 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0 
          RX bytes:3520 (3.4 KiB)  TX bytes:3520 (3.4 KiB)

大功告成。

特别提示:如果是禁止了ROOT远程连接,那么就使用普通用户加sudo的方式结合在expect大量分发。有的同学们问,既然做密钥认证了,为什么还要expect分发呢。在做认证期间,第一次的密钥分发,如果机器数量多,比如1000台,就可以expect分发,比ssh-copy-id方式或http的方式都会好一点。

当然从例一可以看出来,即使不用密钥认证,expect依然可以实现分发数据及文件及批量管理部署服务。

本文发布于Cylon的收藏册,转载请著名原文链接~

链接:expect使用案例

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」 许可协议进行许可。