企业主机服务器日常运维工作中,经常需要登录并以 root 方式执行系统操作,如果在主机数量少的情况下,手工方式登录并执行效率尚可,但如果主机数量庞大(如笔者运维的国外客户服务器数量达 2000+),依次对一台台服务器进行手工操作工作量巨大且出错概率与主机数量成线性增大。 本文分析了在大数量企业服务器情况下,利用 shell 管道,Java SSHD 开源包,Expect 脚本三种方式实现自动登录并执行系统运维操作,三种方式分别适用于不同的场景,可以满足绝大多数企业主机服务器自动化运维的工作内容,大大减轻了系统管理员的工作量,同时降低了操作失误的风险。 本文中的三种方式的代码示例稍作修改,即可直接用于实际的生产主机的运维工作。

采用自动化脚本进行企业服务器运维的原因

传统 Unix 主机服务器手工运维方式的问题

大型企业的 IT 基础设施中,Unix 主机服务器占据重要的地位。在日常运维工作中,经常需要登录并以 root 方式执行系统操作。如果在主机数量少的情况下,手工方式登录并执行运维工作的效率尚可,但如果主机数量庞大(如笔者运维的国外客户服务器数量达 2000+),依次一台台服务器的登录和操作工作量巨大,且出错的概率随服务器数量的增加的递增。

采用自动化脚本进行运维的特点及好处

在大数量 Unix 企业服务器情况下,为了提高运维工作效率,减低手工操作而出错的风险,采用自动化脚本进行运维是一个很好的方式。通过向脚本提供主机列表,用户名账户名密码等输入参数方式,让其自动登录远程目标主机并执行相应的运维操作,批量处理所有涉及的企业服务器主机。该方式下系统管理及运维人员只需要在自动化脚本中提供一次系统运维操作的步骤命令,设定主机列表及正确的帐号密码输入参数,利用 nohup 方式后台运行该脚本,在完成后监控该脚本的输出日志就可以完成上千台服务器的重复运维工作,并大大减轻了系统管理及运维人员的工作量,降低了重复操作中出错的风险。

本文介绍了利用 shell 管道,Java SSHD 开源包,Expect 脚本三种方式实现自动登录并执行系统运维操作的案例。三种方式分别适用于不同的场景,如 shell 管道方式适用于 Telnet/FTP 协议登录的普通账户,SSHD 开源包适用于 ssh1/ssh2 安全登录协议下的登录运维,Expect 脚本适用于 SSHD 协议且需要 sudo 切换到 root 帐号权限的运维操作。

自动化脚本运维的具体实现

利用 shell 管道进行自动化运维

在企业 Unix 服务器上如果开放了 telnet 或者 FTP 协议,通常可以利用 shell 的 EOF 和 << 管道功能将后续的输入作为子命令或子 Shell 的输入,直到遇到 EOF 为止,再返回到主调 Shell,当 s h e l l 看到 < < 的时候,它就会知道下一个词是一个分界符。在该分界符以后的内容都被当作输入,直到 shell 又看到该分界符 ( 位于单独的一行 )。这个分界符可以是你所定义的任何字符串

例如:

<

(你需要执行的操作内容)

EOF

利用该功能,可以将需要 ftp 或者 telnet 登录的运维操作做成自动化脚本,将本来需要交互式输入的帐号和密码及登录后需要的操作指令包在 EOF 和 << 管道符中,以实现自动化 ftp 或者 telnet 到多台服务器并执行。

考虑如下场景:一批客户的 Unix 服务器主机要打 patch,需要将 patch 包用 ftp 上载到服务器指定目录,服务器数量巨大(超过 1000+),单个手工的上载操作是不现实的,因此我们使用 shell EOF 和管道功能编写自动化 ftp 脚本

autoftp 脚本示例如下:

清单 1. autoftp.sh 脚本示例

#!/bin/bash # 指定 ftp 服务器的 i serverip=192.168.1.159 # 指定 ftp 服务器的 ftp 用户 ftpuser=ftptest # 指定 ftp 服务器的 ftp 用户密码 ftppwd=123456 # 指定 client 主机本地下载文件存放的目录 localdir=/home/xiutuo/ftp # 指定 server 主机的 ftp 目录 remotedir=/opt/IBM/DB2/ # 登录 server 主机的 ftp -v -n $serverip << EOF > /tmp/autoftp.log.2010.XX.XX 2>&1 set head off set echo off set wrap off # 指定 ftp 用户和密码 $ftpuser $ftppwd # 指定 server 主机的 ftp 目录和本地目录 lcd $localdir cd $remotedir bin # 上传 patch 包文件至 server 主机的指定目录 put patchXXX.tar.gz EOF

如上可以看到 FTP 登录和上传不再需要手工与每台 server 交互。读者可以修改该脚本,让服务器主机 IP 或主机名通过读配置文件循环获得,从而实现对多台服务器主机的操作。也可以修改 ftp 用户 / 密码部分代码,改成读取输入参数,以增强安全性。

Java SSHD 开源包自动化运维

上述的 Ftp,telnet 管道方式的运维是简单可行的,但是现在业界大型的企业处于安全性的考虑,逐步淘汰此类协议的登陆方式,而改用基于公钥体系的 SSHD 登陆协议。关于 SSHD 协议具体内容已超出本文涉及的范围,请各位感兴趣的读者参考 open-ssh 官方网站

在 SSHD 下的登陆是不允许 shell 管道方式的(如果允许的话意味着 ssh 跟 telnet,ftp 一样丧失了安全性),在此种情况下如果系统管理员要进行自动化运维操作,可以采用 Java 开源的 SSHD 包来进行。

Java ganymed 开源包是成熟的 SSHD 客户端,采用封装 socket 编程方式进行底层的 ssh 通信协议,用户调用其 API 与自己使用 SSHD 命令行登陆服务器的步骤和方法都一致,很容易理解和掌握。开源包的很多 Demo 实例,使即使对 Java 编程不熟悉的系统管理人员,也可以通过简单的修改 demo 代码来实现自身需求的自动化运维操作。

考虑如下案例:一个企业的所有服务器需要将 /etc/services 文件备份至 /usr/local/etc/ 特定逻辑卷目录,企业服务器都采用 SSH2 安全协议,不允许 telnet,ftp 登录。

用 ganymed 的 SSH2 开源包编写自动化登陆脚本,以管理员账号和密码登陆企业 Unix 服务器,执行 cp 操作进行备份。

ganymed 的 Java 代码示例如下:

清单 2. AutoCp.java 类代码示例

import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import ch.ethz.ssh2.Connection; import ch.ethz.ssh2.Session; import ch.ethz.ssh2.StreamGobbler; public class AutoCopy { public static void main(String[] args) { String hostname = "9.212.XXX.XXX; String username = "SolarisAdmin"; String password = "********"; try { /* 创建 SSH2 连接实例 */ Connection conn = new Connection(hostname); /* 打开主机 ssh 端口连接(默认 22) */ conn.connect(); /* 认证方式为 user/passwd */ boolean isAuthenticated = conn.authenticateWithPassword(username, password); if (isAuthenticated == false) throw new IOException("Authentication failed."); /* 已连接到远程主机,打开 session 会话 */ Session sess = conn.openSession(); /* 执行备份操作 */ sess.execCommand(\ "cp /etc/services /usr/local/etc/services; \ ls -lt /usr/local/etc/|grep -i services >&2"); /* 远程主机输入输出流 */ InputStream stdout = new StreamGobbler(sess.getStdout()); InputStream stderr = new StreamGobbler(sess.getStderr()); BufferedReader stdoutReader = new BufferedReader(new InputStreamReader(stdout)); BufferedReader stderrReader = new BufferedReader(new InputStreamReader(stderr)); System.out.println("process result on remote server:"); while (true) { String line = stdoutReader.readLine(); if (line == null) break; System.out.println(line); } System.out.println("Process error from remote server:"); while (true) { String line = stderrReader.readLine(); if (line == null) break; System.out.println(line); } /* 关闭会话 */ sess.close(); /* 关闭连接 */ conn.close(); } catch (IOException e) { e.printStackTrace(System.err); System.exit(2); } } }

以上可以看到,ganymed 的 SSH2 开源包使用简单,建立 connection 并打开会话 session 后,都是普通的对 session 的 IO 操作,读操作则可以获取服务器端的输出,而写操作则对应对服务器端的敲入命令,一目了然,简便实用。

熟悉 Java 编程的系统管理员可以很快上手,即使是没有 Java 基础的管理员也可简单的修改示例中的 sess.execCommand("cp /etc/services /usr/local/etc/services; ls -lt /usr/local/etc/|grep -i services >&2"); 行操作代码来实现自己的业务需求。

具体的 ganymed API 请参考 ganymed project 官方网站

Expect 脚本自动化运维

考虑如下案例情况:有一批客户服务器需要做一个变更,将 sudoer 配置文件从 /etc/sudoer 目录搬移备份到 /usr/local/etc/sudoer 目录,由于该配置文件比较重要,需要 root 权限才能执行搬移操作,该客户服务器也采用 SSHD 协议,不允许 ftp/telnet 等的登录。

该情况下上文所提到的第二种方式 Java 开源包已不适用,因在 shell 进程下当切换用户尤其是 sudo 到 root 时,会 fork 一个新的进程,sudo 到 root 的会话在新的进程中进行,而 Java 开源包是限定在一个会话中的,执行 sudo 操作后,新的进程已经脱离了 Java 开源包的控制,这时候再试图用 Java 开源来执行后续命令,将会报错。

此类需要切换用户的情况下我们采用 Expect 脚本来进行模拟交互。

在这里将 Expect 脚本简介如下:

Expect 使用 Tcl 作为语言核心。不仅如此,不管程序是交互和还是非交互的,Expect 都能运用。这是一个小语言和 Unix 的其他工具配合起来产生强大功能的经典例子。Expect 是一个控制交互式程序的工具。它解决了上述需要用户角色转换的问题,用非交互的方式实现了所有交互式的功能。

Expect 被设计成专门针和交互式程序的交互。一个 Expect 程序员可以写一个脚本来描述程序和用户的对话。接着 Expect 程序可以非交互的运行“交互式”的程序。写交互式程序的脚本和写非交互式程序的脚本一样简单。Expect 还可以用于对对话的一部分进行自动化,因为程序的控制可以在键盘和脚本之间进行切换。

简单的说,Expect 脚本是用一种解释性语言写的。( 也有 C 和 C++ 的 Expect 库可供使用,但这超出了本文的范围 ).Expect 提供了创建交互式进程和读写它们的输入和输出的命令。它是在 Tcl 基础上创建起来的,并提供了一些 Tcl 所没有的命令。

编写 Expect 脚本的基本方式如下:

表 1. Expect 脚本基本使用示例

Expect 命令 作用 使用示例
spawn 激活一个 Unix 程序来进行交互式的运行 . spawn ssh 192.168.1.2
send 向进程发送字符串 send "sudo -s\r"
set 给 Expect 脚本中的变量赋值 set username “joe”
expect 等待进程收到的远程主机的输出,并匹配对应的字符串 , 一旦匹配,执行后续的操作 expect { "yes/no" send "yes" ;}

Expect 还能理解一些特殊情况,如超时和遇到文件尾。 :set timeout 60 ;expect eof

我们以上述的实例作为例子,来看看 Expert 脚本如何实现自动化登陆并 sudo 到 root,然后搬移文件的功能。

Expect 脚本 autoMove 示例如下:

清单 3. autoMove.sh 脚本示例

#!/usr/bin/expect # 导入 Expect 类库 set hostname [lindex $argv 0] # 设置操作的远程主机,$argv 类似 Shell 函数中的接收参数 [lindex $argv 0] # 则表示第一个接收参数 , 例如 expectExample.sh host1 set username [lindex $argv 1] # 同上,第二个接收参数为登陆用户名 set passwd [lindex $argv 2] # 同上,第三个接收参数为登陆用户密码 set timeout 60 # 设置等待超时为 60 秒 spawn ssh $username@$hostname # 使用 spawn 命令来激活 ssh 程序,模拟终端的输出将能够被 Expect 所读取,模拟终端也能从 send 输入到远程主机 expect { "yes/no" {send "yes ";exp_continue} "Password:" {send "$passwd ";} } #Expect 语句等待远程主机的字符串匹配,当匹配到了“yes/no” #则执行后面的操作 .expect 搜索模式"*password:",其中 * 允许匹配 # 任意输入,所以对于避免指定所有细节而言是非常有效的。如果远程主机没有 action, #所以 Expect 检测到该模式后就继续运行。 一旦接收到提示后,下一行就就把密码送给当前进程。 send "sudo -s\r" expect "Password:" {send "$newpasswd\r"} # 执行 sudo 用户角色转换操作 send "copy /etc/sudoers /usr/local/etcsudoers\r" # 执行实际运维操作 send "exit\r " send "exit\r " expect eof {exit 1}

由上我们可以看出 Expect 使用伪终端来和派生的进程相联系。伪终端提供了终端语义以便程序认为他们正在和真正的终端进行 I/O 操作。使用 Expect 等待远程主机的响应并匹配需要的字符串,当匹配到后执行 send 操作向远程主机发送命令,set 操作为赋值,脚本的编写于通常的 Shell 脚本很类似,相当简洁和实用。

在 AIX ,Solairs 的 Unix 平台环境下 Expect 是默认安装的,Linux 需要安装对应的 rpm 包。

总结和展望

以上分析了大型企业服务器的自动化脚本运维,通过不同的案例分别介绍了 shell 管道,Java SSHD 开源包和 Expect 脚本三种方式的自动化运维。三种方式针对不同的业务需求及客户服务器实际环境,有很强的实用性和操作性。可以满足绝大多数企业服务主机的自动化运维工作内容,三种方式的代码示例稍作修改,即可直接用于实际的生产主机日常运维工作中。

如今随着 IT 运维管理工作的复杂度和难度的大大增加,将纯粹的人工操作变为一定程度的自动化管理是一个必然趋势。未来的 IT 自动化运维必将更加专业化、标准化和流程化。通过自动化运维监控,系统能及时发现故障隐患,主动的告诉用户需要关注的资源,以达到防患于未然。通过自动化运维诊断,能最大限度地减少维修时间,提高服务质量。