更多精彩内容请访问我的新博客站点
概述
最近的项目中有远程登录虚拟机并执行相关命令的需求,所以尝试了远程免密码登录虚拟机(在另一篇博客中有介绍链接)。但是发现这还不够,因为登录远程虚拟机后执行的脚本可能会有需要交互的操作,比如sudo
命令需要输入密码,所以就想实现一个完全自动化的脚本,包括登录时的密码自动输入以及登录后执行命令的自动交互。查阅相关资料后学习到,在Linux中可以用expect
来实现自动化的交互,且在python中也有相应的一个模块pexpect具有此功能。本文将先介绍shell自动化交互脚本的实现,然后介绍其python程序实现。
shell脚本
Expect介绍
-
简要介绍:
Expect 是由Don Libes基于
Tcl(Tool Command Language)
语言开发的,主要应用于自动化交互式操作的场景,借助expect处理交互的命令,可以将交互过程如:ssh登录,ftp登录等写在一个脚本上,使之自动化完成。尤其适用于需要对多台服务器执行相同操作的环境中,可以大大提高系统管理人员的工作效率 。 -
主要命令:
-
spawn:启动新的进程
spawn
命令会fork
一个子进程去执行command
命令,然后在此子进程中执行后面的命令;spawn
后的send
和expect
命令都是和spawn
打开的进程进行交互的。如果没有spawn
语句,整个Expect
就无法进行下去,当然,如果真的不要spawn
过程也没有关系,虽然这样就没有办法单独执行,但是这个脚本可以与任何调用它的进程进行交互。**使用方法:**ssh自动登录脚本中,通过
spawn ssh user_name@ip_addr
,fork
一个子进程执行ssh登录命令;连接远程ftp服务器,spawn ftp ftp.server.com
。 -
expect:从进程接收字符串
expect
命令是Expect解释器的关键命令,它的一般用法为expect “string”
,即期望获取到string
字符串,可在在string
字符串里使用 * 等通配符。**使用方法:**在执行
spawn
命令ssh登录时,子进程会要求输入密码,因此可以使用expect
命令检查子进程中的输出中是否包含“password”
子字符串,命令为expect "password"
。 -
send:用于向进程发送字符串
send命令的一般用法为 send “string”,它们会我们平常输入命令一样向命令行输入一条信息,当然不要忘了在string后面添加上 \r 表示输入回车 。
**使用方法:**在使用expect命令接收到字符串“password”后,就需要使用send命令来发送“PASSWORD”。
-
interact:允许用户交互
interact
命令很简单,执行到此命令时,脚本fork
的子进程会将操作权交给用户,允许用户与当前shell进行交互,让人在适当的时候干预这个过程了 。**使用方法:**直接在脚本适当的位置加入一行
interact
。
-
-
安装方法:
$ sudo apt-get install expect
脚本编写
我需要实现一个脚本,其功能是ssh登录虚拟机,并在远程虚拟机用户目录下记录远程登录的日志文件,然后修改其iptables
规则,禁止转发tcp 22号端口的报文。
-
ssh登录
spawn ssh $user@$ip expect { "(yes/no)" {send "yes\r"; exp_continue} "password:" {send "$password\r"} }
expect中可能会接收到两种字符串,"(yes/no)“表示你的主机还未登录过远程虚拟机,即你的用户目录下的文件
~/.ssh/know_hosts
中还未记录该远程虚拟机,问你是否需要将其添加到know_hosts
中,回复"yes”,下次再登录就不会出现这个提醒了;然后就会收到"password:"
,这时就需要将密码发送过去,这样就已经登录到远程虚拟机。 -
记录日志文件
expect "*$" {send "echo 'login +1' >> ~/remote_login.log\r"}
登录之后会收到"$“或”#"的命令行提示符,然后就可以发送需要执行的命令了。
-
修改
iptables
规则expect "*$" {send "sudo iptables -A FORWARD -p tcp --dport 22 -j REJECT\r"} expect "password" {send "$password\r"}
-
退出子程序
expect "*$" {send exit\r}
完整的脚本:
#!/usr/bin/expect
set ip 192.168.1.75
set user openstack
set passwd 123456
set timeout 5
spawn ssh $user@$ip
expect {
"(yes/no)" {send "yes\r"; exp_continue}
"password:" {send "$password\r"}
}
expect "*$" {send "echo 'login +1' >> ~/remote_login.log\r"};
expect "*$" {send "sudo iptables -A FORWARD -p tcp --dport 22 -j REJECT\r"};
expect "password" {send "$passwd\r"};
expect "*$" {send exit\r};
expect eof;
python程序
Pexpect介绍
Pexpect 是Expect 的一个 Python 实现,是一个用来启动子程序,并使用正则表达式对程序输出做出特定响应,以此实现与其自动交互的 Python 模块。 Pexpect 的使用范围很广,可以用来实现与 ssh、ftp 、telnet
等程序的自动交互;可以用来自动复制软件安装包并在不同机器自动安装;还可以用来实现软件测试中与命令行交互的自动化。
其依赖 pty module
,所以 Pexpect
还不能在 Windows 的标准 python 环境中执行,如果想在 Windows 平台使用,可以使用在 Windows 中运行 Cygwin 做为替代方案。
pexpect
主要包含两个接口,一个是run()
函数,另一个是spawn
类。spawn
类的功能很强大,run()
函数要更简单,更适用于快速调用程序。
-
spawn class
使用这个类来开始和控制子程序。
-
spawn
的构造函数class spawn: def __init__(self,command,args=[],timeout=30,maxread=2000,\ searchwindowsize=None, logfile=None, cwd=None, env=None)
spawn
是Pexpect
模块主要的类,用以实现启动子程序,它有丰富的方法与子程序交互从而实现用户对子程序的控制。它主要使用pty.fork()
生成子进程,并调用exec()
系列函数执行command
参数的内容。 使用示例:child = pexpect.spawn('/usr/bin/ftp') child = pexpect.spawn('/usr/bin/ssh user@example.com')
由于需要实现不断匹配子程序输出,
searchwindowsize
指定了从输入缓冲区中进行模式匹配的位置,默认从开始匹配。 -
使用
pexpect
控制子程序-
expect()
定义expect(self, pattern, timeout=-1, searchwindowsize=None)
在参数中:
pattern
可以是正则表达式,pexpect.EOF
,pexpect.TIMEOUT
,或者由这些元素组成的列表。需要注意的是,当pattern
的类型是一个列表时,且子程序输出结果中不止一个被匹配成功,则匹配返回的结果是缓冲区中最先出现的那个元素,或者是列表中最左边的元素。使用timeout
可以指定等待结果的超时时间 ,该时间以秒为单位。当超过预订时间时,expect
匹配到pexpect.TIMEOUT
。expect
不断从读入缓冲区中匹配目标正则表达式,当匹配结束时pexpect
的before
成员中保存了缓冲区中匹配成功处之前的内容,pexpect
的after
成员保存的是缓冲区中与目标正则表达式相匹配的内容。print child.before print child.after
-
send
系列函数send(self, s) sendline(self, s='') sendcontrol(self, char)
这些方法用来向子程序发送命令,模拟输入命令的行为。 与
send()
不同的是sendline()
会额外输入一个回车符 ,更加适合用来模拟对子程序进行输入命令的操作。 当需要模拟发送“Ctrl+c”
的行为时,还可以使用sendcontrol()
发送控制字符。child.sendcontrol('c')
由于
send()
系列函数向子程序发送的命令会在终端显示,所以也会在子程序的输入缓冲区中出现,因此不建议使用expect
匹配最近一次sendline()
中包含的字符。否则可能会在造成不希望的匹配结果。 -
interact()定义
interact(self, escape_character = chr(29), input_filter = None, output_filter = None)
Pexpect还可以调用
interact()
让出控制权,用户可以继续当前的会话控制子程序。用户可以敲入特定的退出字符跳出,其默认值为“^]”
。
-
-
-
run() function
-
run()
的定义run(command,timeout=-1,withexitstatus=False,events=None,\ extra_args=None,logfile=None, cwd=None, env=None)
函数
run
可以用来运行命令,其作用与Python os
模块中system()
函数相似。run()
是通过Pexpect spawn
类实现的。 -
使用run()执行命令svn命令
from pexpect import * run ("svn ci -m 'automatic commit' my_file.py")
与
os.system()
不同的是,使用run()
可以方便地同时获得命令的输出结果与命令的退出状态 。 -
run()的返回值
from pexpect import * (command_output, exitstatus) = run ('ls -l /bin', withexitstatus=1)
command_out
中保存的就是 /bin 目录下的内容。
-
更多关于pexpect的内容请看pexpect.
-
安装python pexpect模块
sudo pip install pexpect
程序编写
还是一样的实现一个程序,其功能是ssh登录虚拟机,并在远程虚拟机用户目录下记录远程登录的日志文件,然后修改其iptables
规则,禁止转发tcp 22号端口的报文。
完整程序:
import pexpect
ip = "192.168.1.75"
user = "chl"
passwd = "123456"
ssh_newkey = "Y|yes/no"
child = pexpect.spawn('ssh %s@%s' % (user, ip))
index = child.expect([pexpect.EOF, pexpect.TIMEOUT, ssh_newkey, "password:"])
if index == 1:
print "TimeoutError!"
if index == 2:
child.sendline("yes")
child.expect("password")
child.sendline(passwd)
if index == 3:
child.sendline(passwd)
child.expect("chl@")
child.sendline("echo 'login +1' >> ~/remote_login.log")
child.expect("chl@")
child.sendline("sudo iptables -A FORWARD -p tcp --dport 22 -j REJECT")
child.expect("password")
child.sendline(passwd)
child.expect("chl@")
child.sendline("exit")
**注:**需要注意的是python中正则表达式与Linux中的通配符是有区别的,不能直接用通配符来编写python正则表达式。