Loading

使用 expect 脚本实现自动化操作

expect 简介

expect 是一个自动化交互套件,主要应用于执行命令和程序时,系统以交互形式要求输入指定字符串,实现交互通信。

在实际工作中,我们运行命令、脚本或程序时,这些命令、脚本或程序都需要从终端输入某些继续运行的指令,而这些输入都需要人为的手工进行。而利用 expect,则可以根据程序的提示,模拟标准输入提供给程序,从而实现自动化交互执行。这就是 expect

expect 自动交互流程:

  1. spawn 启动指定进程
  2. expect 获取指定预期输出
  3. send 发送指定字符串
  4. 执行完成退出

我在日常中使用主要是在 Windows 上使用 Cygwin Terminal, Cygwin Terminal 上通过安装 expect 实现在 Windows 上执行一些自动化运维操作. 用于作为平替 Xshell 的一种简单方案.

expect 命令

  • spawn - 命令用来启动新的进程,spawn后的sendexpect命令都是和使用spawn打开的进程进行交互。
  • expect - 获取匹配信息,匹配成功则执行 expect 后面的程序动作。
    • exp_continue - 在 expect 中多次匹配就需要用到。
  • send - 命令接收一个字符串参数,并将该参数发送到进程。
    • send exp_send - 用于发送指定的字符串信息。
  • interact - 开启人工交互, 比如使用 expect 自动登陆后你想要执行一些手动操作可以设置 except
  • send_user - 用来打印输出, 但不会向进程中输入 相当于 shell 中的 echo
  • [lindex $argv 0] 获取expect脚本的第1个参数
  • set - 定义变量。
    • set timeout - 设置超时时间。
  • puts - 输出变量。
  • exit - 退出 expect 脚本
  • eof - expect 执行结束,退出。
  • set send_human - 在一些场景下,比如通过堡垒机登录时, 如果直接 send 字符会导致不可用, 此时可以设置 send_human 变量是使得 send 自动输入的字符行为更加符合人手工输入的行为. 通过 send -h 可以实现类人

expect 多分支语法

expect 关键字作为 expect 脚本中的关键命令可以实现等待指定字符串后并执行相关程序动作, 通过 expect 可以灵活组合实现不同的效果.

expect "aaa" {send "AAA\r"}
expect "bbb" {send "BBB\r"}
expect "ccc" {send "CCC\r"}

以上脚本是 expect 的基本使用, 首先脚本会等待 字符串'aaa' , 当接收到 aaa 后会发送 AAA; 然后脚本会等待字符串'bbb', 当接收到 bbb 后会发送 BBB; 最后脚本会等待字符串 ccc 并执行相关操作. 其中脚本等待 aaa, bbb ccc 的动作不会同时执行, 而是先等待 aaa 执行相关操作后再执行等待bbb, 最后是 ccc. 其中如果未等待到 aaa(即 timeout 超时后)才会继续等待 bbb操作.

等效于:

等待 aaa 出现时, 发送 AAA
等待 bbb 出现时, 发送 BBB
等待 ccc 出现时, 发送 CCC

其中等待 aaa,bbb,ccc 的操作时按照顺序执行的, 等待 aaa 期间不会同时等待 bbb.

我们再看一种 expect 的多分支语法结构:

expect {
  "aaa" {send "AAA\r"}
  "bbb" {send "BBB\r"}
  "ccc" {send "CCC\r"}
}

如果将 expect 后面跟一个语句块后则会组成一个 expect 分支结构. 脚本再执行 expect 代码块时会等待输出字符串, 如果匹配到 aaa,bbb,ccc 其中之一的话则进入相关分支执行相关命令, 其他未被匹配到的命令不再被执行. 通常这种结构多用于一个命令的执行结果有多种不同的可能情况下.

等效于:

等待 aaa 或 bbb 或 ccc
    当 aaa 出现时, 发送 AAA, 结束后跳出 expect 
    当 bbb 出现时, 发送 BBB, 结束后跳出 expect 
    当 ccc 出现时, 发送 CCC, 结束后跳出 expect 

expect 后面跟语句块常常用于匹配一个命令的不同执行结果场景, 如下面这个脚本, 就是一个基本的 expect 分支结构的用法, 在ssh登录后等待字符串, 如果 匹配到 password 则输入密码登录, 如果匹配到 permission denied 则直接退出

spawn ssh -o StrictHostKeyChecking=no super@$host
expect "*password:" { send "$password\r" }
expect {
  "*Permission denied*" { send_user "unauth or pass error. exit.." exit 1}
  "*~]$*"
}
interact

关于 expect 语句块还有一种用法, 就是可以在后面跟 exp_continue 命令, 用于表示不跳出expect块而是继续等待新的匹配结果.

expect {
  "aaa" { send "AAA\r"; exp_continue }
  "bbb" { send "BBB\r"; exp_continue }
  "ccc" { send "CCC\r" }
}

可以通过 exp_continue 脚本在 expect 代码块种实现循环等待. exp_continue 表示在执行完语句后不退出 expect 代码块, 而是继续等待监听字符串后再执行相关操作. 相当于 for(;;){ if match continue; else do sone thing } 结构中的 continue 操作.

等效于:

等待 aaa 或 bbb 或 ccc
    当 aaa 出现时, 发送 AAA, 结束后继续重新等待匹配字符串
    当 bbb 出现时, 发送 BBB, 结束后继续重新等待匹配字符串
    当 ccc 出现时, 发送 CCC, 结束后跳出 expect 

可以通过 exp_continue 结构实现两次输入密码的逻辑.

set password 123456
spawn ssh -o StrictHostKeyChecking=no root@$host
expect "*password:" { send "$password\r" }
expect {
  "*Permission denied*" { send_user "unauth or pass error. exit.." exit 1}
  "*~]$*"
}
expect "*\]#" { send "password super" }
expect {
    "*password:" { send "$password" exp_continue }
    "*token manipulation error" { }
    "* tokens updated successfully." { }
}
interact

个性化脚本设置

设置 send_human 可以配置在向进程发送字符串时默认人工输入

当前我有这样一个脚本:

#!/usr/bin/expect -f
set timeout 10
set host [lindex $argv 0]
spawn ssh -o StrictHostKeyChecking=no admin@$host
expect "*password:" { send "123456\r" }
expect "\$" { send "su\r" }
interact {
    \001 {send_user "The date is [clock format [clock seconds]]."}
    \002 {send_user "you typed a control-B\n"; exit }
    ˜˜
}

我在使用 expect 中配置使用 sudo su 对节点进行提权时遇到了一些问题, 通过 su 进行提权时总是卡在输入密码的阶段, 抛出 permission denied. 这是因为在执行 su\r 时, 由于输入流过快, 系统误认为 \r 也是密码的一部分, 导致判断密码输入错误从而导致无法通过密码校验. 此时就可以通过设置 send -h 使得命令输入更加类似人类输入的效果从而修复该问题.

  1. 首选需要配置系统变量 send_human:
set send_human {.1 .3 1 .05 2}
  1. 然后在需要实现类人输入的位置指定 send -h 选项实现该效果. 脚本如下:
#!/usr/bin/expect -f
set timeout 10
set send_human {.1 .3 1 .05 2}
set host [lindex $argv 0]
set password 123456
spawn ssh -o StrictHostKeyChecking=no admin@$host
expect "*password:" { send "$password\r" }
expect "\$" { send -h "su\r" }
expect "*]#" {}
interact {
    \001 { send_user "The date is [clock format [clock seconds]]." }
    \002 { send_user "auto pass exit\n"; exit }
    ˜˜
}

interact 实现人工交互

expect 可以通过关键字 interact 将终端的控制权交回给用户, 使得用户可以实现终端交互操作. 由于 expect 主要是实现自动化脚本. 因此需要进入人工交互场景实际并不多.

我们可以在 interact 中自定义按键的行为, 用于实现一些快捷操作, 比如我这里配置了如下快捷键:

  • CtrlA(\001) 实现查看当前时间
  • CtrlB(\002) 实现快速退出
  • 输入 $name 时, interact 会自动帮我们替换为配置的名字 ghimi.
interact {
    \001   {send_user "The date is [clock format [clock seconds]]."}
    \002   { exit }
    \$name {send "ghimi"}
    ˜˜
}

下面脚本使用 expect 打开一个 bash进程, 并配置按键的自动化:

#!/usr/bin/expect -f
set timeout 10
spawn bash
interact {
    \001   {send_user "The date is [clock format [clock seconds]].\n"}
    \002   {send_user "auto bash exit\n"; exit }
    \$name {send "ghimi"}
    ˜˜
}

bash 嵌入执行 expect

#!/bin/bash
hostname=$1
password=$2
set timeout 60
# 加载expect文件路径
/usr/bin/expect <<-EOF
spawn ssh root@${hostname}
expect {
    "(yes/no)" { send "yes\r"; exp_continue }
    "*password" { send "${password}\r" }
}
expect "*]#" { send "exit\r" }
expect eof
EOF
# expect结束标志,EOF前后不能有空格

expect 脚本示例

使用 expect 通过 ssh 自动登录节点

下面是 expect 脚本用于自动登录的功能, 并且实现自动提权至 root

#!/usr/bin/expect -f
set timeout 10
set host [lindex $argv 0]
spawn ssh -o StrictHostKeyChecking=no admin@$host
expect "Opt or ID>:" { send "$host\r" }
expect "ID>:" { send "0\r" }
expect "*opsadmin@*" { send -h "su\r" }
interact {
    \001     {send_user "The date is [clock format [clock seconds]]."}
    \002   {send_user "you typed a control-B\n"; exit }
    ˜˜
}

该脚本实现了使用 admin 用户登录远程主机, 然后通过 sudo su 实现提权至 root 账户下.

将上述脚本保存成 auto_ssh.sh 并赋予 chmod 700 权限, 使用 ./auto_ssh.sh 192.168.1.1 (其中 192.168.1.1 替换成你想要登录的远程节点 ip 地址).

使用 expect 通过 ssh 登录节点实现批量节点自动运维

#!/usr/bin/expect -f
set timeout 10
set ip_list {192.168.1.1 192.168.1.2 192.168.1.3}
foreach ip $ip_list {
    spawn ssh -o SgrictHostKeyChecking=no root@$host
    expect "Opt or ID>:" { send "$host\r" }
    expect "ID>:" { send "0\r" }
    expect "*opsadmin@*" { send -h "su\r" }
}

使用 expect 通过 ssh 登录节点实现批量节点自动运维

#!/usr/bin/expect -f
set timeout 10
set ip [lindex $argv 0]
set user [lindex $argv 1]
set password [lindex $argv 2]
spawn ssh "$user\@$ip" ifconfig
expect "password" { send "$password\r"}
interact

讲脚本保存为 remote.exp 后可以通过 expect remote.exp 192.168.1.1 root 123456 使用了.

使用 send_human 模拟人工输入

#!/usr/bin/expect -f
set timeout 10
set send_human {.1 .3 1 .05 2}

spawn ssh remote_node01
expect "*root]#" { send -h "ifconfig\r" }
expect "*root]#" { }
#!/usr/bin/expect -f
set timeout 10
set send_human {.1 .3 1 .05 2}

spawn bash
expect "ghimi*" { send -h "date\r" }
expect "ghimi*" { }

参考资料

posted @ 2025-04-03 09:32  ghimi  阅读(289)  评论(0)    收藏  举报