文学化的 devops(literate-devops)
运维服务一般分为两个阶段:
- 绞尽脑汁直到服务器能正常工作;
- 努力引入一些自动化工具,例如 Puppet 和 Chef,以提高效率。
最近我一直在致力于使这两个阶段结合更加紧密。由于词穷,我只能将其称为文学化的 devops。
我曾在 《org-mode’s literate programming model》 一文中讨论过研究出来的一些新想法和总结出来的思路,这些想法和思路为我总结出高超的系统管理技巧提供了良好的帮助。
给大家讲个故事
很久以前……我得到了给一个 RPM 包打补丁的任务,这不是我擅长的事情,所以最初的计划是这样的:
- 利用 Vagrant(一个基于 Ruby 的工具,用于创建和部署虚拟化开发环境)来创建一个一次性的 CentOS 系统;
- SSH 远程访问这个虚拟机下载需要的工具;
- 运行各种难以言表的命令;
- 推翻重来并反复进行上述过程直到成功。
当然,一旦我找到了解决方法,我需要为团队中的其他成员记录下这些过程,如果条件允许的化我还会创建可重复执行的脚本。
实现这些的第一步是在其它地方很好的记录下连接配置,不过现在我学到了这个小魔法使你可以直接记录 SSH 连接你用 Vagrant 创建的虚拟机的配置信息(注:clientvm 是我在 Vagrantfile 文件中制定的虚拟机名称):
vagrant ssh-config --host clientvm >> $HOME/.ssh/config
文学化的 Devops
不同于以往打开终端连接到虚拟机的方式,现在我会进入 Emacs,打开这些脚本记录文件[fn:1],创建一个新的标题,并把 shell 和 ruby 等命令输入到这个文件中。
这样做有什么好处呢?不同于传统的终端方式,这样做可以让我执行、记录、追溯任何一条命令。
举个例子,以下是我的 Emacs 窗口的一部分截图:
作为一个记性不太好的老家伙,我的“文学作品”可以解释每一条命令的背景和目的。点击一个超链接,就可以追溯到我之前记录下来的发现结果。一套组合键就能执行需要的代码块……
没错,我就是直接在 Emacs 中执行需要的命令。
敲击两次 Control-C(Emacs 快捷键“C-c C-c”)执行需要的代码(根据不同的语言执行)。上图的例子是在 Shell 中执行的。执行结果会显示在文件中,执行结果同时也可以被其它代码块调用(当然也可以有 其它多种选择 )。
再举个例子,下图是一个从软件仓库中下载 GPG 密钥的片段的前半部分(在这个片段中指定的 URL 被安置在了一个作用于整个代码块的属性中):
这段 Shell 脚本使用 wget 命令下载 HTML 索引文件,分析和提取出密钥文件的 URL。我们将使用一个 Ruby 脚本来完成解析工作,鉴于脚本可能还有点粗糙,我们暂时将其定义在了另一个代码块中。
在 《literate programming》 中 提到了一个想法是将一个代码块插入到另一个代码块中的技术(通过将名字放入双层角括号进行引用,<<...>>)。Donald Knuth 称这一功能为 WEB。由于这有可能和某些语言(例如 Ruby)产生预料不到的冲突,我默认情况下会将其关闭,但在这个代码块中我通过 noweb 参数将其打开。
下图是 Ruby 脚本。将其放在一个指定为 Ruby 的代码块中,我就可以施展所有 Emacs 允许我施展的 Ruby 魔法了。
最后一步是将第一个脚本处理过的 URL 数据传递给另一个 Shell 脚本,来调用 wget 下载每一个 URL 指向的文件:
key-list 被定义在最初的代码块中,作为代码块执行结果的名字。我们声明了一个 LIST 变量用来保存执行结果列表,这样 shell 脚本就可以通过$LIST 的形式访问 LIST 变量。
上述的例子演示了文学化的编程是如何将不同的语言代码和数据糅合在一起产生作用的。
如果是虚拟机的话该怎么办呢?
通常情况下,通过指定代码块语言为 sh 的方式来告诉 Emacs 在本地系统 shell 中执行代码。但是在这个案例中,我希望代码能够在虚拟机上执行(或在我圈养开发服务器上)。接下来我会介绍两种方式来实现这一切,一种是使用 Tramp ,而另一种则是使用各种 Sessions。
** 搬出 Tramp 这个救兵
Tramp 是 Emacs 提供的一种功能,它允许使用 ssh 或者其它协议访问和编辑远程主机上的文件。例如你可以输入以下命令,在远程主机上执行 find-file 功能(Emacs 快捷键“C-x C-f”):
/ssh:howard.abrams@goblin.howardism.org:web/files/robot.txt
前提是你需要在.emacs 启动文件中放入如下内容:
(setq tramp-default-method "ssh")
;; linux 系统下默认就是 ssh 可以不用设置
;; windows 系统下将 ssh 改为 plink(putty 的一个模块)。当然 windows 系统嘛……你懂的,祝你好运!!!
同时更新你的~/.ssh/config 文件,声明你希望用什么用户访问你指定的服务器,简单来说文件内容就是如下的样子:
/goblin.howardism.org:web/files/robot.txt
Emacs 会通过“:”符号后面的内容来决定 Tramp 要访问的目标。使用 SSH keys 能方便 Tramp 的访问,否则就要在提示符下输入密码。
每个 org-mode 代码块都可以设置“:dir”来指定代码片段在哪个目录执行。就如同下图中代码块的例子:
:dir
配置项支持 Tramp 的全部功能,允许我在不同的主机上执行代码块。还记得我是怎么将 Vagrant 虚拟机的连接信息加入到我的~/.ssh/config 文件中的么?
但是如果我要访问防火墙后面的主机该怎么办呢?
我的工作是需要搞定部署在受到严密保护的数据中心的虚拟机,首先我需要登录到跳板机或者堡垒主机上。Tramp 也可以按序处理这些跳跃,比如下面的例子:
/ssh:10.98.18.229|ssh:10.0.1.122|sudo:/etc/network/interfaces
先使用我的用户登录到堡垒主机,然后再使用我的用户登录到运行在私有云的虚拟机上。再然后使用 sudo 命令让我编辑 root 权限才能编辑的文件。
在 org-mode 代码块的“:dir”配置项中,也可以使用 Tramp 的管道符号“|”:
一些需要记住的技巧:
- 在最后一跳不能使用管道符号“|”,而是要用冒号“:”来指定需要访问的目标。
- 当你使用管道符号“|”的时候,记得声明使用的协议,哪怕使用的协议是默认的。
- 如果你本地主机的操作系统和远程主机的操作系统不一致,你可能需要修复 org-mode 中的一个 bug,你可以在注脚 2 里找到修复的方法[fn:2]。
利用 org-mode 会话
另一种处理方式是创建一个会话将不同的代码块串联起来。下图每一个代码块都使用一个相同的会话“client”(正好是我的虚拟机的主机名“Client”):
当我执行第一个代码块的时候,后台会启动一个 shell,它会 ssh 链接到主机。需要注意的是,要想让这些生效,你需要将你的 ssh 公钥放入到远程 系统的“.ssh/authorized_keys”文件中以开启免密码访问,或者使用 Emacs 的 ssh 插件包。[fn:3]
这样一来,我利用“client”会话执行的每一个代码块都会通过这个连接在远程主机上执行(在这个例子中是虚拟机,但这没有关系,其原理都是一样的)。
上述的两种方法都工作的很好,但是第二种方法允许我设定变量,以创建其他代码块可以利用的特定连接状态。
我还有第三种方法,使用 ob-screen 来处理[fn:4],其交互性更强,但是不允许传递变量给代码,正如你在下文所看到的,这对我来说很有用。
不管使用哪种方法,我都是通过一边记录一边验证每一步操作的方式来渐进明细的实现我的目标。最终结果可以发布到 web 或者 wiki 上去。
对于冗长的命令应该怎么办?
有时候执行的命令会非常耗时而且内容冗长,而往往我又需要将执行结果放入 Emacs 窗口中从而更方便的在执行结果中搜索需要的内容。
这种情况我通常会使用一种可以折叠的“drawer”(一种定义输出内容开始和结尾的方式):
请将光标移动到“drawer”处,敲击 Tab 键就可以隐藏或显示输出内容:
可以利用输出的内容么?
某些命令常常会使用到上一条命令的输出结果,而且我敢肯定你习惯于使用鼠标来复制粘贴这些输出结果,但是我有更好的方法。
在下面这个例子中,我需要一个 RPM 包依赖关系的列表:
请注意,我给这个代码块定义了名字。同时也请注意 Emacs 自动分解了输出结果并整理到了表格中。默认情况下 shell 命令的输出结果会按照换行符和空格分开。
我可以将这次执行的输出结果传递给另一个代码块。接下来的代码块创建了一个名为 DEPENDS 的变量,平且使用了之前输出的第一列的第 2 至第 10 行的数组作为值。
当我下载 RPM 包的时候,我不希望使用鼠标来进行交互操作。
设置变量和赋值
重复利用 devops 程序(就像大厨的食谱一样) 的一个关键因素是将代码和代码所使用的值进行解偶。这也是重复利用任何程序的关键因素。
在我的工作中,我会为每次尝试创建一个新的 org-mode 文件,每一个任务或问题都会有自己的标题和段落。在每个段落中会定义一系列的属性,包括作用于整个段落中所有代码块的变量。
为了创建段落变量,只需要简单的敲击“C-c C-x p”,然后设置名称为“var”的属性,然后以“变量名=值”的形式将值赋值给变量,就像下面这个例子:
host="10.52.224.33"
这一系列属性可以包括任何你想要的代码块的值,如会话或者结果等。这些值也可以设置在代码块中进行覆盖,就像你将在下面截图中看到的:
通过设置变量或进行其他设置(尤其是会话设置),可以将代码块关联起来。
与他人进行沟通
在日常运营或系统管理(正如我之间描述的)中,如果相关问题领域调查研究出了有用的信息,我需要针对调查结果和我的组员进行沟通交流。我的 Emacs 配置文件 允许我通过 Emacs 发送邮件,我开启了 org-mime-org-buffer-htmlize 功能,可以将我的 org-mode 文件导出成为 HTML 格式的邮件正文(这个功能由最新版的 org-plus-contrib 插件包提供)。
当然,有些时候导出的 HTML 邮件正文也不是非常令人满意。
举个例子,某些代码块会输出一段 JSON 数据,如果我指定输出格式为 JavaScript 的时候,HTML 输出结果就会高亮显示,这样看起来会更美观方便。只要使用“wrap”参数,就像下图这样:
[文件内容](http://howardism.org/Technical/Emacs/literate-devops-20.txt)
像我的 org-mode 文件中这样:
文件内容:http://howardism.org/Technical/Emacs/literate-devops-21.txt
导出结果就会像如下这样:
{"time":{"iso":"2015-05-19T23:12:40Z","timestamp":1432077160,"date":"19 May 2015","time":"7:12 PM"}}
再举一个例子,我现在的项目中使用到了 OpenStack,其 nova 命令行工具会将输出数据格式化为表格:
+--------------------------------------+--------------------+--------+------------+-------------+------------------------+
| ID | Name | Status | Task State | Power State | Networks |
+--------------------------------------+--------------------+--------+------------+-------------+------------------------+
| f9e7aed8-e425-4808-aace-8758dadd91bf | chefserver | ACTIVE | - | Running | WPC-private=10.0.1.73 |
| 0432f8b1-7e6d-4fc1-b181-02fa768c38ac | ha-compute1 | ACTIVE | - | Running | WPC-private=10.0.1.104 |
| a5bdd1d0-d4b3-4856-a657-5759356c186b | ha-controller1 | ACTIVE | - | Running | WPC-private=10.0.1.97 |
| 16263972-609e-44c0-83e0-f3147336071c | ha-controller2 | ACTIVE | - | Running | WPC-private=10.0.1.99 |
| 89a89d1f-7be5-4c4f-82db-64b751f15f3b | ha-controller3 | ACTIVE | - | Running | WPC-private=10.0.1.100 |
| b740095a-3f89-45d0-a2a1-9cfcadfb4ca3 | ha-monitoring | ACTIVE | - | Running | WPC-private=10.0.1.95 |
| 6bebe823-1504-4cb1-a898-bbc7894b1a32 | ha-sdn-controller1 | ACTIVE | - | Running | WPC-private=10.0.1.101 |
| 456bf417-580e-49fb-be08-1b0153710f86 | ha-sdn-controller2 | ACTIVE | - | Running | WPC-private=10.0.1.102 |
| 7aab184c-5fb4-4996-8ab2-8a65ea7668cb | ha-sdn-controller3 | ACTIVE | - | Running | WPC-private=10.0.1.103 |
| 0c90d7b0-dab4-4af8-a970-e2e90dd8b9e4 | ha-storage-1 | ACTIVE | - | Running | WPC-private=10.0.1.76 |
| fda0666e-d656-48fd-928f-83fb47c923f2 | ha-storage-2 | ACTIVE | - | Running | WPC-private=10.0.1.81 |
| 021fc9c1-8d79-4c09-b3d4-6014d242403a | ha-storage-3 | ACTIVE | - | Running | WPC-private=10.0.1.96 |
| bc5ad0fe-9ef2-4966-8d2b-99892f3f94cd | yum-server | ACTIVE | - | Running | WPC-private=10.0.1.74 |
+--------------------------------------+--------------------+--------+------------+-------------+------------------------+
如果你手动修改输出,这些修改在文件导出的时候将不会生效(因为输出命令在导出过程中将被重新执行)。
想解决这一问题,你只需要将以下一小段 Emacs Lisp 代码放入你的 org-mode 文件中:
[文件内容](http://howardism.org/Technical/Emacs/literate-devops-22.txt)
这个代码块被命名为“nova-conv”,我可以用它来预处理导出结果,就像下图这样:
[文件内容](http://howardism.org/Technical/Emacs/literate-devops-23.txt)
在我这个例子中,我也干掉第一列中的破折号,使其更有 org-mord 的范儿。
[文件内容](http://howardism.org/Technical/Emacs/literate-devops-24.txt)
为了让这些代码真的可以被重用,你需要将其放入 Library of Babel (一个类似 org-mode 函数库的地方),这样就可以被任何文件调用了。
总结
当然我的文学化 devops 方法是不能完全替代自动化 DevOps 的,但是我发现这个方法有两点可取之处:
- 这是一个在些手册之前很好的记录的方式;
- 这是一个在遇到问题时能方便快捷的撰写组内邮件的方式。
关于最后一点,我总是会在写我的执行代码之前编写我的“文学”文件,就像下面这样:
[文件内容](http://howardism.org/Technical/Emacs/literate-devops-25.txt)
如果我接下来的命令或者过程执行失败,我可以通过简单的方法将段落设置为高亮,敲击“C-x M”将文件导出为 HTML 格式,邮件发送给其他的组员(否则我需要话大量的时间从终端上复制粘贴,以便邮件能提供足够的上下文)。
如果需要完整的例子,可以查看我的文章 《notes on setting up IP Tables》 (或者 《original org-mode file》 ),里面有一部分内容可以在编辑器中执行,以便查看我的主机是如何设置的,还有一部分执行并重置主机防火墙规则的脚本。
感谢您的阅读!
脚注
[fn:1]
原文: http://howardism.org/Technical/Emacs/literate-devops.html#fnr.1
我会为每个新脚本创建一个 org-mode 格式的文件 ,用来跟踪任务,备注和记录其它细节。这使得它非常契合文学化 devops。
[fn:2]
原文: http://howardism.org/Technical/Emacs/literate-devops.html#fnr.2
每一个系统都会在不同的目录创建临时文件。大部分 Unix 系统在/tmp/目录下,而 Macs 系统在/var/folders/目录下。目前的 org-mode 代码,在远程系统上使用和本地系统一样的目录名。在文中的例子里,我使用 Mac 笔记本连接到数据中心的 Linux 系统上,而我会得到如下 错误:
Tramp: Decoding remote file `/ssh:x.y.z:/var/folders/0s/pcrc3rq5075gj4tm90pbh76c36sl1h/T/ob-input-32379ujY' using `base64 -d -i >%s'...failed
byte-code: Couldn't write region to `/ssh:x.y.z:/var/folders/0s/pcrc3rq5075gj4tm90pbh76c36sl1h/T/ob-input-32379ujY', decode using `base64 -d -i >%s' failed
我在 《this mailing list posting》 这篇文章中,发现这个 bug 存在于 8.2.10 版本及之前版本的 org-mode(在找到最佳的解决方案之前,我估计这个 bug 暂时还不会被修复)。你可以通过自行编辑 ob-core.el 文件的 org-babel-temp-file 函数来修正这个问题:
(defun org-babel-temp-file (prefix &optional suffix)
"Create a temporary file in the `org-babel-temporary-directory'.
Passes PREFIX and SUFFIX directly to `make-temp-file' with the
value of `temporary-file-directory' temporarily set to the value
of `org-babel-temporary-directory'."
(if (file-remote-p default-directory)
(let ((prefix
;; We cannot use `temporary-file-directory' as local part
;; on the remote host, because it might be another OS
;; there. So we assume "/tmp", which ought to exist on
;; relevant architectures.
(concat (file-remote-p default-directory)
;; REPLACE temporary-file-directory with /tmp:
(expand-file-name prefix "/tmp/"))))
(make-temp-file prefix nil suffix))
(let ((temporary-file-directory
(or (and (boundp 'org-babel-temporary-directory)
(file-exists-p org-babel-temporary-directory)
org-babel-temporary-directory)
temporary-file-directory)))
(make-temp-file prefix nil suffix))))
[fn:3]
原文: http://howardism.org/Technical/Emacs/literate-devops.html#fnr.3
如果你安装了 ssh.el 这个包,你可以通过使用“M-x ssh”来初始化你的远程系统连接。
你需要输入主机的连接信息,包括密码(如果需要的话)和其他信息。举个例子,如果我要连接到我的远程主机: goblin.howardism.org ,我创建会话的代码会是如下这个样子:
,```h :session *ssh goblin.howardism.org* :var dir="/opt"
ls $dir
,```
这段代码允许你查看远程服务器上的代码执行结果,同时也允许你调用从其它 org-mode 文件读取的完整功能代码块。
注释:会话参数使用“*”包住(包括一部分 buffer name),但如果你想传递变量的话需要使用引号包住(否则,它将被视为文档中其它位置表格的引用名)。
[fn:4]
原文: http://howardism.org/Technical/Emacs/literate-devops.html#fnr.4
我有第三种方法来远程执行命令,这种方法使用 ob-screen 扩展(它包含在 org-mode Contrib 中)。它同时使用 Gnu screen 和 xterm,所以在我的 Mac 上,我启动了 XQuartz(一个内置的 X Windows 模拟器),并将下述代码块加入我的.emacs 启动文件中(根据 这个 说明 )来设置我 xterm 程序的完整路径。
(setq org-babel-default-header-args:screen
'((:results . "silent")
(:session . "default")
(:cmd . "bash")
(:terminal . "/opt/X11/bin/xterm")))
我虽然不经常使用 screen,但我还是用 Homebrew 安装了它:
brew install screen
然后告诉 ob-screen 如何找到 screen:
(setq org-babel-screen-location "/usr/local/bin/screen")
这下代码块就被指定为调用 screen,接下来我通常会通过设置“:session parameter”来指定调用哪个 xterm 窗口。
,```creen :session blah
ls /Applications
,```
运行结果不会显示在我的 Emacs 窗口,而是显示在 xterm 窗口中。
使用 screen 的一个缺点就是不能传递变量,比如下面的例子就不行:
,```creen :session blah :var dir="/Applications"
ls $dir
,```
来回切换 Emacs 和 xterm 窗口来查看结果还是比较方便的,但是不能将结果返回到文件做进一步处理会比较受限。同时你需要抵御通过在 xterm 窗口中输入来修改命令的诱惑,如果你改了,你有可能会因忘记把修改后的信息保存回 org-mode 文件中,而后悔不及。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

上一篇: 黑帽百科 PDF 文档
下一篇: 我是怎样使用 Emacs 的
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论