自动化脚本与程序实现

概述

最近的项目中有远程登录虚拟机并执行相关命令的需求,所以尝试了远程免密码登录虚拟机(在另一篇博客中有介绍链接)。但是发现这还不够,因为登录远程虚拟机后执行的脚本可能会有需要交互的操作,比如sudo命令需要输入密码,所以就想实现一个完全自动化的脚本,包括登录时的密码自动输入以及登录后执行命令的自动交互。查阅相关资料后学习到,在Linux中可以用expect来实现自动化的交互,且在python中也有相应的一个模块pexpect具有此功能。本文将先介绍shell自动化交互脚本的实现,然后介绍其python程序实现

shell脚本

Expect介绍

  1. 简要介绍:

    Expect 是由Don Libes基于Tcl(Tool Command Language)语言开发的,主要应用于自动化交互式操作的场景,借助expect处理交互的命令,可以将交互过程如:ssh登录,ftp登录等写在一个脚本上,使之自动化完成。尤其适用于需要对多台服务器执行相同操作的环境中,可以大大提高系统管理人员的工作效率 。

  2. 主要命令:

    • spawn:启动新的进程

      spawn命令会fork一个子进程去执行command命令,然后在此子进程中执行后面的命令;spawn后的sendexpect命令都是和spawn打开的进程进行交互的。如果没有spawn语句,整个Expect就无法进行下去,当然,如果真的不要spawn过程也没有关系,虽然这样就没有办法单独执行,但是这个脚本可以与任何调用它的进程进行交互。

      使用方法:ssh自动登录脚本中,通过spawn ssh user_name@ip_addrfork一个子进程执行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

  3. 安装方法:

    $ sudo apt-get install expect

脚本编写

我需要实现一个脚本,其功能是ssh登录虚拟机,并在远程虚拟机用户目录下记录远程登录的日志文件,然后修改其iptables规则,禁止转发tcp 22号端口的报文。

  • ssh登录

    1
    2
    3
    4
    5
    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:",这时就需要将密码发送过去,这样就已经登录到远程虚拟机。

  • 记录日志文件

    1
    expect "*$" {send "echo 'login +1' >> ~/remote_login.log\r"}

    登录之后会收到”$”或”#”的命令行提示符,然后就可以发送需要执行的命令了。

  • 修改iptables规则

    1
    2
    expect "*$" {send "sudo iptables -A FORWARD -p tcp --dport 22 -j REJECT\r"}
    expect "password" {send "$password\r"}
  • 退出子程序

    1
    expect "*$" {send exit\r}

完整的脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/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()函数要更简单,更适用于快速调用程序。

  1. spawn class

    使用这个类来开始和控制子程序。

    • spawn的构造函数

      1
      2
      3
      class spawn:
      def __init__(self,command,args=[],timeout=30,maxread=2000,\
      searchwindowsize=None, logfile=None, cwd=None, env=None)

      spawnPexpect模块主要的类,用以实现启动子程序,它有丰富的方法与子程序交互从而实现用户对子程序的控制。它主要使用 pty.fork() 生成子进程,并调用 exec() 系列函数执行 command 参数的内容。 使用示例:

      1
      2
      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.EOFpexpect.TIMEOUT ,或者由这些元素组成的列表。需要注意的是,当 pattern 的类型是一个列表时,且子程序输出结果中不止一个被匹配成功,则匹配返回的结果是缓冲区中最先出现的那个元素,或者是列表中最左边的元素。使用 timeout 可以指定等待结果的超时时间 ,该时间以秒为单位。当超过预订时间时, expect 匹配到pexpect.TIMEOUT

        expect 不断从读入缓冲区中匹配目标正则表达式,当匹配结束时 pexpectbefore 成员中保存了缓冲区中匹配成功处之前的内容, pexpectafter 成员保存的是缓冲区中与目标正则表达式相匹配的内容。

        1
        2
        print child.before
        print child.after
      • send系列函数

        1
        2
        3
        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() 让出控制权,用户可以继续当前的会话控制子程序。用户可以敲入特定的退出字符跳出,其默认值为“^]”

  2. run() function

    • run()的定义

      1
      2
      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命令

      1
      2
      from pexpect import *
      run ("svn ci -m 'automatic commit' my_file.py")

      os.system() 不同的是,使用 run() 可以方便地同时获得命令的输出结果与命令的退出状态 。

    • run()的返回值

      1
      2
      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号端口的报文。

完整程序:

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
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正则表达式。