使用 Emacs Lisp 管理和部署服务器

最近一段时间都在做各种项目部署、服务器部署,顺便分享下我是如何用 Emacs 以及编写 Emacs Lisp 代码来管理和部署数台服务器的。

说到自动化,可能平时大家爱用比如 pdsh 命令,或者 Python 的 Fabric 框架,再或者是 tmux、screen,这些都可以很方便地为我们做很多自动化的事情。Emacs 内置了多种交互式 shell 方案,eshell、shell 和 ansi-term,我们让 shell 交互式运行在 Emacs 中,就可以像操作 buffer 那样方便地来操控 shell了。平时我常用 shell(注,这里的 shell 指的是在 Emacs 中执行:M-x shell),然后我可以很方便地写 Emacs Lisp 代码来自动化一些事情。

现在,有 n 台服务器,我想在 Emacs 中开启多个 buffer,并在每个 buffer 中打开一个 shell,然后 ssh 到远程服务器上。并且每个 buffer 名就是主机名,这样,我要管理某台主机,直接 C-x b,然后输入主机名补全,就可以切换到对应那台主机的 shell了。代码如下:

(defun ssh->hosts ()
  "SSH到列表中的主机"
  (dolist (host *hosts*)
    (shell host)
    (insert (format "ssh %s" host))
    (comint-send-input)))

上面代码做了以下事情:

  1. 遍历 *hosts* 变量,*hosts* 变量是我定义的一个全局列表,里面每个元素是服务器的 IP 地址和主机名;
  2. 执行 shell 函数,shell 函数会新打开一个 buffer,并在 buffer 中创建一个交互式 shell 环境(支持 bash、zsh 等)。buffer 的名字就是 shell 函数的第一个参数,即主机名或 IP 地址。
  3. 然后在新建的 shell 下,执行一条 shell 命令:ssh 主机地址。comint-send-input 函数是回车并执行 shell。

以下就是我定义的 *hosts* 变量:

(defvar *hosts* '("172.17.0.5" "172.17.0.8"))

现在,我要部署服务器,希望在每台服务器上,都批量执行同样的命令,于是又写了一个函数:

(defun run (command)
  "执行shell命令"
  (let ((current-buf (current-buffer)))
      (dolist (host *hosts*)
        (shell host)
        (insert command)
        (comint-send-input))
      (switch-to-buffer current-buf)))

run 函数遍历 *hosts* 里对应的每个 buffer,然后执行我们指定的 shell 命令。比如以下调用:

(run "sudo apt-get upgrade")

就可以批量升级系统。

这里,我录了一段视频来演示。视频中,我用 Docker 启动了两台虚拟机,演示了如何在两台机器上同步执行命令(注意视频右边上下两个 window,分别是不同主机的 ssh)。

接着,我新建了个叫“hello”的 buffer,然后重新绑定了回车键,在 buffer 中输入的每一行内容都作为要执行的命令,按下回车后,会在两台虚拟机上执行。

(视频地址:http://www.tudou.com/programs/view/CEuYyiO-f8Y/

<embed src="http://www.tudou.com/v/CEuYyiO-f8Y/&bid=05&rpid=722398072&resourceId=722398072_05_05_99/v.swf" type="application/x-shockwave-flash" allowscriptaccess="always" allowfullscreen="true" wmode="opaque" width="480" height="400"></embed>

视频里的两个函数 newline-and-submit 和 current-line-string 代码如下:

(defun newline-and-submit ()
  "获取并作为 shell 命令执行当前行的内容,然后插入换行"
  (interactive)
  (let ((line (current-line-string)))
    (run line)
    (reindent-then-newline-and-indent)))

(defun current-line-string ()
  "获得当前行的字符串内容"
  (interactive)
  (buffer-substring-no-properties (line-beginning-position) (line-end-position)))