一、概述
Expect是一个免费的编程工具语言,用来实现自动和交互式任务进行通信,而无需人的干预。Expect的作者Don Libes在1990年开始编写Expect时对Expect做有如下定义:Expect是一个用来实现自动交互功能的软件套件。通过expect系统管理员可以创建脚本用来实现对命令或程序提供输入,而这些命令和程序是期望从终端(terminal)得到输入,一般来说这些输入都需要手工输入进行的。Expect则可以根据程序的提示模拟标准输入提供给程序需要的输入来实现交互程序执行。甚至可以实现实现简单的BBS聊天机器人。
我们通过Shell可以实现简单的控制流功能,如:循环、判断等。但是对于需要交互的场合则必须通过人工来干预,有时候我们可能会需要实现与SSH、FTP服务器等进行免交互的自动连接功能,而Expect正是用来实现这种功能的工具。
Expect是不断发展的,随着时间的流逝,其功能越来越强大,已经成为系统管理员的的一个强大助手。Expect需要Tcl编程语言的支持,所以要在系统上运行Expect必须首先安装Tcl。
通过ssh远程操控主机时交互解决方式:
-
通过ssh的密钥对
-
通过sshpass工具提交密码
-
通过expect工具提交密码
二、安装expect
root@localhost \~\]# rpm -q expect \[root@localhost \~\]# yum -y install expect
三、如何使用expect
在使用expect程序前,我们首先简单说下expect在常规使用中的工作流程:
首先expect的内部命令spawn启动指定进程-->expect获取期待的关键字-->内部命令send向指定进程发送响应内容-->进程执行完成后,退出expect程序。
通过上面expect的工作流程,我们可以认识到,expect的内置命令:spawnexpectsend等非常重要,所以下面我们先来了解下它们的使用。
3-1、spawn命令
spawn作用:启动新的产生交互的进程
spawn命令的语法:
spawn [选项] [需要执行的shell命令或程序等]
下面我们以修改一个已存在账户的密码为例,举例如下:
root@localhost \~\]# useradd tom \[root@localhost \~\]# passwd tom 更改用户 tom 的密码 。 新的 密码: 无效的密码: 密码未通过字典检查 - 过于简单化/系统化 重新输入新的 密码: passwd:所有的身份验证令牌已经成功更新。
基于expect交互界面做法
root@localhost \~\]# expect expect1.1\> spawn passwd tom spawn passwd tom 1388 expect1.2\> expect "密码:" 更改用户 tom 的密码 。 新的 密码:expect1.3\> send "123456r" expect1.4\> expect "密码:" 无效的密码: 密码少于 8 个字符 重新输入新的 密码:expect1.5\> send "123456r" expect1.6\> expect eof passwd:所有的身份验证令牌已经成功更新。 expect1.7\> exit
3-2、expect命令
expect作用:获取从spawn命令执行的命令和程序后产生的交互信息。看看是否匹配,如果匹配,就开始执行expect进程接收字符串。
expect命令的语法:
expect [选项] 表达式 [动作]
选项:比如"-re"表示使用正则表达式来进行匹配。
案例:
root@localhost \~\]# expect expect1.1\> spawn passwd tom spawn passwd tom 1437 expect1.2\> expect "新的 密码:" 更改用户 tom 的密码 。 新的 密码:expect1.3\> send "123456r" expect1.4\> expect "新的 密码:" 无效的密码: 密码少于 8 个字符 重新输入新的 密码:expect1.5\> send "123456r" expect1.6\> expect eof passwd:所有的身份验证令牌已经成功更新。 expect1.7\> exit
3-3、send命令
send命令的主要作用是,在expect命令匹配完指定的字符后,发送指定的字符串给系统程序,在字符中可以支持部分特殊转义符,比如:n(回车)r(换行)t(制表符)等。
案例
root@localhost \~\]# expect expect1.1\> spawn passwd tom spawn passwd tom 1437 expect1.2\> expect "新的 密码:" 更改用户 tom 的密码 。 新的 密码:expect1.3\> send "123456r" expect1.4\> expect "新的 密码:" 无效的密码: 密码少于 8 个字符 重新输入新的 密码:expect1.5\> send "123456r" expect1.6\> expect eof passwd:所有的身份验证令牌已经成功更新。 expect1.7\> exit
3-4、exp_continue命令
exp_continue命令的主要作用是,如果需要一次匹配多个字符串,那么多次匹配字符串并执行不同的动作中,可以让expect程序实现继续匹配的效果。
案例:
root@localhost \~\]# vim chang_pass2.exp #!/usr/bin/expect # Filename : change_pass2.exp spawn passwd tom expect { "新的 密码:" { send "123456r"; exp_continue } "新的 密码:" { send "123456r" } eof } \[root@localhost \~\]# chmod +x chang_pass2.exp \[root@localhost \~\]# ./chang_pass2.exp spawn passwd tom 更改用户 tom 的密码 。 新的 密码: 无效的密码: 密码少于 8 个字符 重新输入新的 密码: passwd:所有的身份验证令牌已经成功更新。
3-5、send_user命令
send_user命令的作用是:用来打印expect脚本信息,类似shell里的echo命令。
root@localhost \~\]# vim send_user.exp #!/usr/bin/expect #Filename: send_user.exp send_user "beijingn" send_user "shanghait" send_user "guangzhoun" \[root@localhost \~\]# chmod +x send_user.exp \[root@localhost \~\]# ./send_user.exp beijing shanghai guangzhou
3-6、expect变量
3-6-1、普通变量
expect中定义普通变量的语法如下:
set 变量名 变量值
例如:
set chengshi "beijing"
调取变量的方法是:
puts $变量名
#或者
send_user "$变量名"
3-6-2、expect中位置参数变量
如何向expect脚本中像shell一样传递类似于0、1等位置参数,用于接收及控制expect脚本传递位置参数变量呢?
expect是通过如下语法来进行的:
set <变量名称> [lindex $argv <param index> ]
当然,除了基本的位置参数外,expect还支持其它的特殊参数,比如:
$argc 表示传入参数的个数
$argv0 表示当前执行脚本的名称。
案例:
root@localhost \~\]# vim expect_var2.exp #!/usr/bin/expect # Filename: expect_var2.exp set file1 \[lindex $argv 0
set file2 [lindex $argv 1]
set file3 [lindex $argv 2]
puts "The files are : file1 file2 $file3"
puts "The number of files are: $argc"
puts "The expect script name is: $argv0"
root@localhost \~\]# chmod +x expect_var2.exp \[root@localhost \~\]# ./expect_var2.exp 1.txt 2.txt 3.txt The files are : 1.txt 2.txt 3.txt The number of files are: 3 The expect script name is: ./expect_var2.exp
3-7、expect中if条件语句
expect中if 条件语句的语法结构如下:
if {条件表达式} {
commands;
}
if {条件表达式} {
commands;
} else {
commands;
}
注意事项:上面条件语句中每个"{"前要至少保证有一个空格。
案例:
root@localhost \~\]# vim expect-if.exp #!/usr/bin/expect # Filename: expect-if.exp if {$argc \<= 3} { puts "The IP numbers \<= 3" } else { puts "The IP numbers \> 3" } \[root@localhost \~\]# chmod +x expect-if.sh \[root@localhost \~\]# ./expect-if.sh 192.168.200.{1..3} The IP numbers \<= 3 \[root@localhost \~\]# ./expect-if.sh 192.168.200.{1..5} The IP numbers \> 3
3-8、expect中常用关键字
3-8-1 eof关键字
eof是和spawn对应的,当spawn发送指令到终端执行起始会有一个eof,等指令在终端完毕后,在返回时eof被expect捕捉,就好比在shell中 cat >>file <<OEFrr content rr EOF一样,在结束时也要有EOF,这样是对应的。因前面案例中已有举例,这里就不再举例说明。
Interact允许用户交互,由管理员结束进程。
3-8-2 timeout关键字
expect脚本我们都知道,首先spawn我们要执行的命令,然后就给出一堆expect的屏幕输出,如果输出匹配了我们的expect的正则匹配内容,我们就会send一个命令上去,模拟用户输入。
但是expect中等待命令的输出信息是有一个timeout的设定的,默认是10秒。这个特性是防止那些执行死机的命令的。一旦到了这个timeout,还是没有屏幕输出的话,expect脚本中下面的代码就会执行。或者我们在expect脚本中如果定义了timeout的响应代码的话,这些代码就会被执行。
解决这样的问题非常简单,最简单的办法就是在expect脚本的开头定义
set timeout -1 -- 永久不超时
set timeout 0 -- 立即执行
set timeout XX -- 设定具体的timeout时间(秒),默认是10秒。
3-9、expect中for循环语句
{ set i 1 } 定义i的值为1
{ $i <= 10 }循环的条件
{ incr i 1} 制定$i的增量值,必须写在这行的末尾处,默认增量值为1
root@localhost \~\]# cat expect-for.exp #!/usr/bin/expect for { set i 1 } { $i \<= 10 } { incr i 1 } { puts "$i" } \[root@localhost \~\]# expect expect-for.exp 1 2 3 4 5 6 7 8 9 10
四、Shell脚本调用expect 的方法
- 在shell脚本中使用expect --c "..."可以在shell中调用expect编程语言;
root@localhost \~\]# cat shell-expect1.sh #!/bin/bash for i in 192.168.200.{112..113} do expect -c " spawn ssh root@$i ifconfig ens32 expect { "yes/no" { send "yes\\r";exp_continue } "password" { send "123456\\n" } } expect eof " done \[root@localhost \~\]# bash shell-expect1.sh spawn ssh root@192.168.200.112 ifconfig ens32 root@192.168.200.112's password: ens32: flags=4163\
mtu 1500 inet 192.168.200.112 netmask 255.255.255.0 broadcast 192.168.200.255 inet6 fe80::20c:29ff:fe8c:f2d9 prefixlen 64 scopeid 0x20\ ether 00:0c:29:8c:f2:d9 txqueuelen 1000 (Ethernet) RX packets 3512 bytes 1656192 (1.5 MiB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 2808 bytes 365154 (356.5 KiB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 spawn ssh root@192.168.200.113 ifconfig ens32 root@192.168.200.113's password: ens32: flags=4163\ mtu 1500 inet 192.168.200.113 netmask 255.255.255.0 broadcast 192.168.200.255 inet6 fe80::20c:29ff:fe0b:7eab prefixlen 64 scopeid 0x20\ ether 00:0c:29:0b:7e:ab txqueuelen 1000 (Ethernet) RX packets 2722 bytes 1511052 (1.4 MiB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 2196 bytes 258197 (252.1 KiB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
2、在shell脚本中使用/usr/bin/expect <<-EOF ... EOF的方式可以调用绝大多数的其它脚本语言,这种方式执行命令建议使用绝对路径,而且要严格遵守expect 的脚本格式
root@localhost \~\]# cat shell-expect2.sh #!/bin/bash for i in 192.168.200.{112..113} do /usr/bin/expect \<\< EOF spawn ssh root@$i ifconfig ens32 expect { "yes/no" { send "yes\\r";exp_continue } "password" { send "123456\\n" } } expect eof EOF done \[root@localhost \~\]# bash shell-expect2.sh spawn ssh root@192.168.200.112 ifconfig ens32 root@192.168.200.112's password: ens32: flags=4163\
mtu 1500 inet 192.168.200.112 netmask 255.255.255.0 broadcast 192.168.200.255 inet6 fe80::20c:29ff:fe8c:f2d9 prefixlen 64 scopeid 0x20\ ether 00:0c:29:8c:f2:d9 txqueuelen 1000 (Ethernet) RX packets 3329 bytes 1632688 (1.5 MiB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 2609 bytes 340022 (332.0 KiB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 spawn ssh root@192.168.200.113 ifconfig ens32 root@192.168.200.113's password: ens32: flags=4163\ mtu 1500 inet 192.168.200.113 netmask 255.255.255.0 broadcast 192.168.200.255 inet6 fe80::20c:29ff:fe0b:7eab prefixlen 64 scopeid 0x20\ ether 00:0c:29:0b:7e:ab txqueuelen 1000 (Ethernet) RX packets 2561 bytes 1488208 (1.4 MiB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 1997 bytes 233005 (227.5 KiB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 \[root@localhost \~\]# cat zhixing.sh #!/bin/bash for i in 192.168.200.{112..113} do /usr/bin/expect \<\< EOF spawn ssh root@$i wget http://192.168.200.111/user20.sh -O /tmp/user20.sh \&\& bash /tmp/user20.sh expect { "yes/no" { send "yesr";exp_continue } "password" { send "123456n" } } expect eof EOF done
五、expect在生产环境中的案例
案例要求:
手边现在有一个统计CPU信息的脚本(cpuinfo.sh),该脚本在单独的物理服务器上或虚拟主机上执行后,能统计该机器CPU的五个相关信息,比如:
root@localhost \~\]# sh cpuinfo.sh logical CPU number in total: 32 //当前机器中逻辑CPU个数为:32 phycical CPU number in total: 2 //当前机器中物理CPU颗数为: 2 core number in a physical CPU: 8 //机器中每颗物理CPU有8核心 logical CPU number in a phycical CPU:16 //机器中每颗物理CPU 有16 个逻辑CPU Hyper threading is enabled. //每颗CPU的超线程已经打开
具体代码如下:
root@localhost \~\]# cat cpuinfo.sh #!/bin/bash # filename: cpu-info.sh #this script only works in a Linux system which has one or more indentical physical CPU(S) echo -n "logical CPU number in total: " #逻辑CPU个数 cat /proc/cpuinfo \|grep "\^processor" \|wc -l #有些系统没有多核也没有打开超线程,就直接退出脚本 cat /proc/cpuinfo \|grep -qi "core id" if \[ $? -ne 0 \]; then echo "Warning. No multi-core or hyper-threading is enabled." exit 0; fi echo -n "phycical CPU number in total: " #物理CPU个数 cat /proc/cpuinfo \|grep "physical id" \|sort \|uniq \|wc -l echo -n "core number in a physical CPU: " #每个物理CPU上的core的个数(未计入超线程) core_per_phy_cpu=$(cat /proc/cpuinfo \|grep "core id"\|sort \|uniq \|wc -l) echo $core_per_phy_cpu echo -n "logical CPU number in a phycical CPU:" #每个物理CPU中逻辑CPU(可能是core,threads或both)的个数 logical_cpu_per_phy_cpu=$(cat /proc/cpuinfo \|grep "siblings"\|sort \|uniq \|awk -F: '{print $2}') echo $logical_cpu_per_phy_cpu #是否打开有超线程 #如果在同一个物理CPU上有两个逻辑CPU具有相同的"core id",那么超线程是打开的 #此处根据前面计算的 core_per_phy_cpu和 logical_cpu_per_phy_cpu的比较来查看超线程 if \[ $logical_cpu_per_phy_cpu -gt $core_per_phy_cpu \]; then echo "Hyper threading is enabled." elif \[ $logical_cpu_per_phy_cpu -eq $core_per_phy_cpu \]; then echo "Hyper threading is NOT enabled." else echo "Error. There's something wrong." fi
下面我们要做的是将这个脚本通过expect批量分发到指定范围的机器上进行自动化跑批量,以便能通过shell脚本自动统计批量采集所有被指定机器上的CPU相关信息。具体脚本实现思路如下:
1)通过expect实现将单一脚本自动免交互上传至指定客户机。
2)通过shell脚本中的循环语句,调取expect单机自动免交互上传文件脚本。从而实现将cpuinfo.sh脚本批量上传到客户机指定目录下。
3)通过expect实现自动免交互方式在指定单台客户机上执行指定命令或脚本程序。
4)通过shell脚本中的循环语句,调取单机免交互自动执行脚本程序的expect脚本。从而实现在多台机器上免交互自动执行shell脚本。
脚本案例如下:
1)通过expect实现将单一脚本自动免交互上传至指定客户机。
root@localhost \~\]# vim auto_upload_file.exp #!/usr/bin/expect # Filename: auto_upload_file.exp if { $argc != 3 } { puts "usage: expect $argv0 file host dir" exit } set file \[lindex $argv 0
set host [lindex $argv 1]
set dir [lindex $argv 2]
set password "123456"
spawn scp -P22 -rp file root@host:$dir
expect {
"yes/no" {send "yes\r"; exp_continue}
"*password*" {send "$password\r"}
}
expect eof
root@localhost \~\]# chmod +x auto_upload_file.exp \[root@localhost \~\]# ./auto_upload_file.exp usage: expect ./auto_upload_file.exp file host dir \[root@localhost \~\]# ./auto_upload_file.exp cpuinfo.sh 192.168.200.112 /opt spawn scp -P22 -rp cpuinfo.sh root@192.168.200.112:/opt root@192.168.200.112's password: cpuinfo.sh 100% 1470 393.6KB/s 00:00
2)通过shell脚本中的循环语句,调取expect单机自动免交互上传文件脚本。从而实现将cpuinfo.sh脚本批量上传到客户机指定目录下。
root@localhost \~\]# vim auto_upload_files.sh #!/bin/bash # Filename : auto_upload_files.sh if \[ $# -ne 2 \]; then echo "useage : sh $0 file dir" exit 1 fi file=$1 dir=$2 for IP in 192.168.200.{112..113} ; do expect auto_upload_file.exp $file $IP $dir done \[root@localhost \~\]# chmod +x auto_upload_files.sh \[root@localhost \~\]# ./auto_upload_files.sh useage : sh ./auto_upload_files.sh file dir \[root@localhost \~\]# ./auto_upload_files.sh cpuinfo.sh /opt/ spawn scp -P22 -rp cpuinfo.sh root@192.168.200.112:/opt/ root@192.168.200.112's password: cpuinfo.sh 100% 1470 783.5KB/s 00:00 spawn scp -P22 -rp cpuinfo.sh root@192.168.200.113:/opt/ root@192.168.200.113's password: cpuinfo.sh 100% 1470 662.7KB/s 00:00
3)通过expect实现自动免交互方式在指定单台客户机上执行指定命令或脚本程序。
root@localhost \~\]# vim auto_run_host.exp #!/usr/bin/expect # Filename: auto_run_1host.sh if { $argc !=2 } { puts "usage: expect $argv0 ip command" exit } set ip \[lindex $argv 0
set cmd [lindex $argv 1]
set password "123456"
spawn ssh root@ip cmd
expect {
"yes/no" {send "yes\r"; exp_continue}
"*password*" {send "$password\r"}
}
expect eof
root@localhost \~\]# chmod +x auto_run_host.exp \[root@localhost \~\]# ./auto_run_host.exp 192.168.200.112 "ifconfig ens32" spawn ssh root@192.168.200.112 ifconfig ens32 root@192.168.200.112's password: ens32: flags=4163\
mtu 1500 inet 192.168.200.112 netmask 255.255.255.0 broadcast 192.168.200.255 inet6 fe80::20c:29ff:fe8c:f2d9 prefixlen 64 scopeid 0x20\ ether 00:0c:29:8c:f2:d9 txqueuelen 1000 (Ethernet) RX packets 2650 bytes 1541675 (1.4 MiB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 1874 bytes 236034 (230.5 KiB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 \[root@localhost \~\]# ./auto_run_host.exp 192.168.200.112 "source /opt/cpuinfo.sh" spawn ssh root@192.168.200.112 source /opt/cpuinfo.sh root@192.168.200.112's password: logical CPU number in total: 1 phycical CPU number in total: 1 core number in a physical CPU: 1 logical CPU number in a phycical CPU:1 Hyper threading is NOT enabled.
4)通过shell脚本中的循环语句,调取单机免交互自动执行脚本程序的expect脚本。从而实现在多台机器上免交互自动执行shell脚本。
root@localhost \~\]# vim auto_run_hosts.sh #!/bin/bash # Filename: auto_run_hosts.sh if \[ $# -ne 1 \]; then echo "useage: $0 cmd" exit 3 fi cmd=$1 for IP in 192.168.200.{112..113}; do expect auto_run_host.exp $IP "$cmd" done \[root@localhost \~\]# chmod +x auto_run_hosts.sh \[root@localhost \~\]# ./auto_run_hosts.sh useage: ./auto_run_hosts.sh cmd \[root@localhost \~\]# ./auto_run_hosts.sh "source /opt/cpuinfo.sh" spawn ssh root@192.168.200.112 source /opt/cpuinfo.sh root@192.168.200.112's password: logical CPU number in total: 1 phycical CPU number in total: 1 core number in a physical CPU: 1 logical CPU number in a phycical CPU:1 Hyper threading is NOT enabled. spawn ssh root@192.168.200.113 source /opt/cpuinfo.sh root@192.168.200.113's password: logical CPU number in total: 1 phycical CPU number in total: 1 core number in a physical CPU: 1 logical CPU number in a phycical CPU:1 Hyper threading is NOT enabled.