赞
踩
项目地址在: https://gitee.com/xiaofeipapa/fabric-tutorial
建议先下载代码, 边看边运行.
fabric 是python的一个组件, 能够在远程服务器运行你写好的脚本, 也就是说这是一个运维自动化工具.
以前运维人员在远程服务器上安装软件, 要先登录 -> 写脚本(如果习惯本地写程序, 那么还要上传程序到服务器) -> 针对服务器反馈输入数据 -> 调试 …
如果用fabric, 这个流程就是全部自动化了. fabric的程序开发流程一般是这样:
如果只需要操作一两台服务器, 那么手动操作肯定比写程序快. 如果要操作几十上百台机器, 那么当然是写好程序, 然后让他们自动在几十台服务器上面跑更方便. (程序员就是要学会偷懒! )
网上能找到的fabric 大多东摘西抄, 程序不能运行. 锅主要由fabric 来背, 一开始他们并不支持 python3, 所以当python3 流行之后, 以前针对 python2 的fabric教程就基本不能用了; 于是有热心程序员们搞了个非官方的fabric3 , 写了一些教程(很少且很多文章是错的), 但后来fabric 官方支持了 python3 , 也出了个教程…
于是在互联网上至少存在3种fabric 教程. 针对python2的, 针对非官方python3版本的, 还有官方python3 官方版本的, 初学者大概会被弄得要发疯了吧…
所以我将这份教程命名为2021版, 表示这是今年最新的版本. 同时这是针对官方python3版本的教材, 如果你不熟悉fabric, 就不用看网上其他教程了, 看这一篇即可.
最后, 这篇教程用的是 python3 的fabric 版本. 因此你本机要先安装python3. 如果是linux 版本, 一般是这样:
sudo apt install python3-pip
安装完成之后, 只要用:
sudo pip3 install xxx
所安装的包/组件就都是python3版本了.
如果是windows系统, 假设你下载安装的就是python3的软件, 那么当你使用 pip install 的时候, 这个pip 就是3的版本. 所以不需要再多设置.
linux:
sudo pip3 install fabric
windows:
pip install fabric
如果你很熟悉docker 或者自己就有云服务器, 那么试验环境就有了. 如果你没有(特别是windows 用户, 很难装好docker) 怎么办.
你可以使用我的这个虚拟机镜像. 首先你要安装 virtualbox, 下载地址在这里: https://www.virtualbox.org/wiki/Downloads
在某云盘下载我的这个虚拟机镜像:
这是个 xubuntu 系统(ubuntu的变种), 我最爱的系统之一.
安装之后用virtualbox 把它导入, 一步步操作如下:
打开virtualbox之后, 选择New
Type选择Linux, Version 选择Ubuntu(64bit) , 然后点击Next.
内存默认 1024 就够了, 点Next
选择"Use an existing … " , 然后点右边那个文件夹图标
在打开的界面中, 选择 Add
然后选择刚才下载的xub.vdi文件, 如图:
然后点 choose
这时候你会看到 create 已经变得可选了, 点它. 就会看到虚拟机已经成功创建.
点击右上方的Settings, 然后在弹出的界面设置网卡:
(中文名字应该是桥接网络) . 这样选了之后, 你的电脑(称为宿主机) 就可以访问虚拟机里的操作系统了.
双击xub 启动系统, 你会看到 xubuntu的界面. 这个系统有两个用户的信息, 分别是:
xiaofeipapa / 密码 aa
root / 密码 test
在xubuntu的桌面点击鼠标右键, 选择 Open Terminal Here, 打开命令行, 输入:
ifconfig
查看自己的ip是多少. 在我这里是 192.168.135.24 .
然后在宿主机里打开命令行, 输入:
ssh root@192.168.135.24
然后输入root的密码(test ) , 就可以看到ssh 登录成功了. 好了, 你拥有一个实验环境, 可以在上面试验fabric了.
ip : 192.168.135.24 ( 也许你的机器会不同, 在虚拟机里用ifconfig来查看)
用户: root / test
以下程序的参数都针对我那个虚拟机镜像, 如果你有自己的虚拟机或者云服务器, 改相应参数即可.
#! /usr/bin/python3 # -*- coding: UTF-8 -*- """ 作者: 小肥爬爬 简书: https://www.jianshu.com/u/db796a501972 gitee: https://gitee.com/xiaofeipapa/python-toolkit 您可以自由转载此博客文章, 恳请保留原链接, 谢谢! """ from fabric import Connection def do_it(): host = '192.168.135.24' user = 'root' password = 'test' # ssh 连接的正确姿势 conn = Connection(host=host, user=user, connect_kwargs={'password': password}) # 在远程机器运行命令(用run方法), 并获得返回结果 # hide 表示隐藏远程机器在控制台的输出, 达到静默的效果 # 默认 warn是False, 如果远程机器运行命令出错, 那么本地会抛出异常堆栈. 设为True 则不显示这堆栈. cmd = 'ls /tmp' result = conn.run(cmd, hide=True, warn=True, encoding='utf-8') # 正常运行时, 信息在 stdout里 print('-------- 下面是 stdout 信息') print(result.stdout.strip()) # 出错时, 信息在 stderr 里 print('-------- 下面是 stderr 信息') print(result.stderr.strip()) if __name__ == '__main__': do_it()
注释里已经把api用法说得很清楚了. 请阅读并运行程序加深印象.
在我们写程序的时候, 其实大部分时候都运行正常. 如果每次run之后都去判断 stdout, stderr … 代码很快变得混乱不堪. 所以我的选择是将之封装成方法, 使之更加轻便. 这个文件命名为 fab_utils.py, 内容如下:
#! /usr/bin/python3 # -*- coding: UTF-8 -*- """ 作者: 小肥爬爬 简书: https://www.jianshu.com/u/db796a501972 gitee: https://gitee.com/xiaofeipapa/python-toolkit 您可以自由转载此博客文章, 恳请保留原链接, 谢谢! 将fabric api 再封装一层, 使之更好用 """ class FabException(Exception): def __init__(self, msg): Exception.__init__(self, msg) # 运行远程命令 def run(conn, cmd, hide=True, warn=True): r = conn.run(cmd, encoding='utf8', hide=hide, warn=warn) result, err = r.stdout.strip(), r.stderr.strip() if err: raise FabException(err) return result
以后, 我会把这个工具库逐渐扩大, 加入更多有用的方法. 经过这种方式, fabric的大多数功能就变成你的工具库, 可以轻松写出大多数的运维程序.
写一个程序 s2_test_fab_utils.py 来测试:
#! /usr/bin/python3 # -*- coding: UTF-8 -*- """ 作者: 小肥爬爬 简书: https://www.jianshu.com/u/db796a501972 gitee: https://gitee.com/xiaofeipapa/python-toolkit 您可以自由转载此博客文章, 恳请保留原链接, 谢谢! """ from fabric import Connection import fab_utils def do_it(): host = '192.168.135.24' user = 'root' password = 'test' # ssh 连接的正确姿势 conn = Connection(host=host, user=user, connect_kwargs={'password': password}) # 结果变得更简单了 cmd = 'ls /tmp' result = fab_utils.run(conn, cmd) # 正常运行时, 信息在 stdout里 print('-------- 下面是结果') print(result) # 出错时, 程序会抛出异常 # 来一个出错的例子, tmp/xiaofeipapa 目录不存在 cmd = 'ls /tmp/xiaofeipapa' result = fab_utils.run(conn, cmd) print(result) if __name__ == '__main__': do_it()
运行这个结果, 你能看到第一个例子成功输出结果, 第二个例子抛出异常, 这就是我想要的效果.
登录密码写在参数里当然不安全. 用linux 系统的同学都知道, 可以配置无密码方式访问服务器, 避免暴露密码明文在参数里. (windows我暂时不知道怎么办, 有待请教. )
首先在宿主机生成密钥, 一路按回车:
ssh-keygen -t rsa
然后运行这个脚本:
ssh-copy-id root@192.168.135.24
输入root的登录密码 test, 以后就可以无密码访问服务器了. 相应地, fabric的连接变成如下:
conn = Connection(host=host, user=user)
不用输入密码了.
像ls, cd 都是bash 预先提供的命令. 如果要运行自己写的shell怎么办? 有两个办法: 如果sh 文件很大, 那么先在本地写好, 然后传到服务器上运行. 如果shell很少, 直接用这个方式运行:
conn.client.exec_command(cmd)
下面的这个例子演示了如何运行短的shell脚本:
#! /usr/bin/python3 # -*- coding: UTF-8 -*- """ 作者: 小肥爬爬 简书: https://www.jianshu.com/u/db796a501972 gitee: https://gitee.com/xiaofeipapa/python-toolkit 您可以自由转载此博客文章, 恳请保留原链接, 谢谢! """ from fabric import Connection import fab_utils def do_it(): host = '192.168.0.12' user = 'root' # ssh 连接的正确姿势 conn = Connection(host=host, user=user) # 运行shell之前, 要随便运行一个命令, 获得运行环境 fab_utils.run(conn, 'uname -a') # 运行shell脚本 # 在shell里 [ -d xxx ] 表示检查文件夹是否存在 # [ -d xxx ] 表示检查文件是否存在 cmd = '[ -d /tmp ] && echo ok' _stdin, _stdout, _stderr = conn.client.exec_command(cmd) result = _stdout.read().strip().decode('utf8') print('--- std out: ') print(result) if __name__ == '__main__': do_it()
bash没有提供检查文件夹是否存在, 文件是否存在的方法, 而这两个方法是经常需要使用的. 所以我们可以用简单的shell脚本来封装这两个方法. 在 fab_utils.py 里加入如下方法:
# 检查文件夹是否存在def is_dir_exists(conn, dir_path): cmd = '[ -d ' + dir_path + ' ] && echo ok' _stdin, _stdout, _stderr = conn.client.exec_command(cmd) result = _stdout.read().strip().decode('utf8') return result == 'ok'# 检查文件文件是否存在def is_file_exists(conn, file_path): cmd = '[ -f ' + file_path + ' ] && echo ok' _stdin, _stdout, _stderr = conn.client.exec_command(cmd) result = _stdout.read().strip().decode('utf8') return result == 'ok'# 判断是否有该文件夹, 不存在时可以通过参数控制生成文件夹def check_has_dir(conn, dir_path, create_if_not_exist=True): if not is_dir_exists(conn, dir_path): if create_if_not_exist: run(conn, 'mkdir -p ' + dir_path)# 判断是否有该文件, 不存在时可以通过参数控制生成文件夹def check_has_file(conn, file_path, create_if_not_exist=True): if not is_dir_exists(conn, file_path): if create_if_not_exist: run(conn, 'touch ' + file_path)
然后写一个python文件来检查:
#! /usr/bin/python3 # -*- coding: UTF-8 -*- """ 作者: 小肥爬爬 简书: https://www.jianshu.com/u/db796a501972 gitee: https://gitee.com/xiaofeipapa/python-toolkit 您可以自由转载此博客文章, 恳请保留原链接, 谢谢! """ from fabric import Connection import fab_utils import os, os.path def do_it(): host = '192.168.0.12' user = 'root' # ssh 连接的正确姿势 conn = Connection(host=host, user=user) # 运行shell之前, 要随便运行一个命令, 获得运行环境 fab_utils.run(conn, 'uname -a') # 检查文件夹 dir_path = '/root/data' fab_utils.check_has_dir(conn, dir_path) # 查看创建文件夹结果 result = fab_utils.run(conn, 'ls ' + dir_path) print(result) # 检查文件 file_path = os.path.join(dir_path, 'done.txt') fab_utils.check_has_file(conn, file_path) # 查看创建文件结果 result = fab_utils.run(conn, 'ls ' + file_path) print(result) if __name__ == '__main__': do_it()
我们都知道在服务器上尽量不要用rm命令, 否则删错文件那就完了. 所以在用fabric的时候我们同样要注意这个问题. 在fab_utils.py 里加入这个方法:
# 在服务器安全删除文件/文件夹的方法 def safe_rm(conn, file_path): # 先检查是否存在 try: run(conn, 'ls ' + file_path) except FabException: print(file_path + ' 不存在') return # 在/tmp下生成当天目录 result = run(conn, 'date +%Y-%m-%d') trash_dir = '/tmp/my_trash/' + result check_has_dir(conn, trash_dir) # 在当天下生成uuid, 确保mv 不会失败 result = run(conn, 'uuidgen') trash_dir = trash_dir + '/' + result check_has_dir(conn, trash_dir) run(conn, 'mv ' + file_path + ' ' + trash_dir)
写一个文件来测试. 程序在 s5_rm_test.py
#! /usr/bin/python3 # -*- coding: UTF-8 -*- """ 作者: 小肥爬爬 简书: https://www.jianshu.com/u/db796a501972 gitee: https://gitee.com/xiaofeipapa/python-toolkit 您可以自由转载此博客文章, 恳请保留原链接, 谢谢! """ from fabric import Connection import fab_utils import os, os.path def do_it(): host = '192.168.0.12' user = 'root' # ssh 连接的正确姿势 conn = Connection(host=host, user=user) # 运行shell之前, 要随便运行一个命令, 获得运行环境 fab_utils.run(conn, 'uname -a') # 删除之前创建的文件夹 /root/data dir_path = '/root/data' fab_utils.safe_rm(conn, dir_path) if fab_utils.is_dir_exists(conn, dir_path): print('--- 文件夹还在') else: print('--- 文件夹已经删除') if __name__ == '__main__': do_it()
在服务器上我们通常会有写文件的任务, 例如写入环境变量到配置文件, 在fabric 里可以用简单的shell命令来完成:
conn.run("echo 'hello' >> /tmp/test.txt")
这个命令又是双引号又是单引号, 所以还是封装起来更好用. 在fab_utils.py 里加入如下方法:
# 追加一行到文件末尾
def append_line(conn, file_path, content):
if not is_file_exists(conn, file_path):
return
cmd = "echo '" + content + "' >> " + file_path
run(conn, cmd)
然后写一个文件来测试, 代码在 s6_env_var.py
#! /usr/bin/python3 # -*- coding: UTF-8 -*- """ 作者: 小肥爬爬 简书: https://www.jianshu.com/u/db796a501972 gitee: https://gitee.com/xiaofeipapa/python-toolkit 您可以自由转载此博客文章, 恳请保留原链接, 谢谢! """ from fabric import Connection import fab_utils def do_it(): host = '192.168.0.12' user = 'root' # ssh 连接的正确姿势 conn = Connection(host=host, user=user) # 运行shell之前, 要随便运行一个命令, 获得运行环境 fab_utils.run(conn, 'uname -a') # 在/tmp 下生成 test_rc 文件, file_path = '/root/test_rc' fab_utils.check_has_file(conn, file_path) # 写入内容到文件里 fab_utils.append_line(conn, file_path, "#==其他配置信息1") fab_utils.append_line(conn, file_path, "#==其他配置信息2") fab_utils.append_line(conn, file_path, "#==其他配置信息3") fab_utils.append_line(conn, file_path, "\n") fab_utils.append_line(conn, file_path, "#==JAVA安装信息") fab_utils.append_line(conn, file_path, "JAVA_HOME=XXX") fab_utils.append_line(conn, file_path, "CLASS_PATH=YYY") fab_utils.append_line(conn, file_path, "export PATH=$PATH:JAVA_HOME/bin") # 查看文件内容 result = fab_utils.run(conn, 'cat ' + file_path) print(result) if __name__ == '__main__': do_it()
有些命令如 passwd , adduser 会等候你继续输入下一步. 像这种需要手动响应的自动化程序应该怎么写? 在fabric 里这样做:
from invoke import Responder
# 其他代码
resp1 = Responder(
pattern=r'Enter new UNIX password:',
response=pwd + '\n'
)
resp2 = Responder(
pattern=r'Retype new UNIX password:',
response=pwd + '\n'
)
conn.run('passwd ' + username, pty=True, hide=True, watchers=[resp1, resp2])
Responder 是 invoke 框架的一部分(fabric 已经包括invoke框架) . 它的使用方法如下:
如果不确定某些命令的pattern和resonse 是什么. 可以先在服务器手动输入命令看看结果, 然后再写程序. 如果程序多次需要输入, 那么就要写多个responder, 用个数组包起来.
注意: 如果程序长时间没有反馈, 证明你的pattern 写得有问题.
最常见的原因就是转义字符没有进行处理. 下面这是一个例子:
# 在yum上安装软件
def yum_install(conn, cmd):
resp = Responder(
# pattern=r'Is this ok [y/d/N]:', # 错误例子: 没有正则转义
pattern=r'Is this ok \[y/d/N\]:',
response='y\n'
)
conn.run(cmd, pty=True, watchers=[resp], hide=True)
下面是一个完整的例子: 我们将用户xiaofeipapa的密码改为 bb, 整个过程是自动化无人工干预的:
#! /usr/bin/python3 # -*- coding: UTF-8 -*- """ 作者: 小肥爬爬 简书: https://www.jianshu.com/u/db796a501972 gitee: https://gitee.com/xiaofeipapa/python-toolkit 您可以自由转载此博客文章, 恳请保留原链接, 谢谢! """ from fabric import Connection import fab_utils from invoke import Responder def do_it(): host = '192.168.0.12' user = 'root' # ssh 连接的正确姿势 conn = Connection(host=host, user=user) # 运行shell之前, 要随便运行一个命令, 获得运行环境 fab_utils.run(conn, 'uname -a') # 将密码改成bb username = 'xiaofeipapa' pwd = 'bb' resp1 = Responder( pattern=r'Enter new UNIX password:', response=pwd + '\n' ) resp2 = Responder( pattern=r'Retype new UNIX password:', response=pwd + '\n' ) conn.run('passwd ' + username, pty=True, hide=True, watchers=[resp1, resp2]) if __name__ == '__main__': do_it()
前面零零散散地介绍了很多fabric的用法, 现在用这个案例把它综合起来: 在远程服务器(虚拟机) 里安装jdk环境.
这个任务做开发/运维的同学应该都不陌生. java技术栈的运维架构通常是1个nginx, 后端挂若干个安装jdk服务器, 以此达到负载均衡的目的.它的任务可以分解为:
当然其中还包括一些弯弯绕绕的必要任务. 例如怎么判断任务已经运行过, 所以不会再重复运行? 怎么判断jdk包已经存在, 不用再重新下载? 怎么判断环境变量已经写入到配置文件, 不用重复写入?
这些技巧一一列举如下.
思路: 给任务一个名字, 例如 install_jdk. 第一次在目标机器运行之后, 就会在 /root 目录下生成这个文件: /root/data/install_jdk/done.txt . 所以程序可以判断这个文件是否存在, 如果不存在表示此任务还没执行过. 如果存在, 那么可以通过命令行参数来指定是否再次运行任务.
代码如下:
def _prepare(self, rerun): """ 检查任务是否已经启动过 :return: """ # 必须要先run连接远程服务器, 否则 conn.client.exec_command 会报错 fab_utils.run(self.conn, 'ls /tmp') # 生成 /root/data 目录 base_dir = '/root/data' fab_utils.create_if_remote_dir_not_exist(self.conn, base_dir) # 检查该task 是否已经执行过. 如果已经执行过, 在 /root/data/task 目录下有 done.txt 文件 task_home = base_dir + '/' + self.task fab_utils.create_if_remote_dir_not_exist(self.conn, task_home) self.task_home = task_home task_file = task_home + '/done.txt' if not fab_utils.is_remote_file_exist(self.conn, task_file): return # 如果存在, 又不指定重新运行, 则退出 if not rerun: title('此程序已经在目标机器运行过了. 如果想再次运行, 请设置启动参数 -r true') exit(0) # ...... 其他代码 # 命令行指定启动参数 if __name__ == '__main__': import argparse parser = argparse.ArgumentParser('传入参数:***.py') parser.add_argument('-r', '--rerun', default=True) # 开发时用 # parser.add_argument('-r', '--rerun', default=False) args = parser.parse_args() rerun = args.rerun print('---- 参数 rerun: ', rerun) do_it(args)
思路: 当第一次下载好jdk包的时候, 我们可以拿到该包的md5, 然后如果服务器上已经存在jdk包并且md5一致, 那么就不用再下载. 否则开始下载jdk.
代码如下:
with self.conn.cd(self.task_home):
# 检查下载jdk
file_path = self.task_home + '/' + jdk_file
file_md5 = 'ef599e322eee42f6769991dd3e3b1a31'
down_link = 'wget https://repo.huaweicloud.com/java/jdk/8u181-b13/' + jdk_file
fab_utils.check_download(self.conn, file_path, file_md5, down_link)
# 创建jvm目录
fab_utils.create_if_remote_dir_not_exist(self.conn, jvm_dir)
# 解压. 华为的这个包解压后名字不同, 所以手动指定一个目录
hint('正在解压 jdk ......')
fab_utils.run(self.conn, 'tar xvf ' + jdk_file + ' -C ' + jvm_dir + ' --strip-components 1 ')
有两个思路:
第一个思路容易错删其他内容, 所以后来采用了第二个思路. 代码如下:
run_result = conn.run('echo $JAVA_HOME ', hide=True).stdout.strip() if run_result == jvm_dir: hint(system_file + ' 已经包含 JAVA_HOME, 不需要重新设置') else: # 追加内容到系统配置 conn.run("echo '# ===== java 配置' >> " + system_file) conn.run("echo 'export JAVA_HOME=" + jvm_dir + "' >> " + system_file) # 将 exporter jar 也加入到 classpath 里 export = 'export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JAVA_HOME/jre/lib/rt.jar:'\ + exporter_jar_path conn.run("echo '" + export + "' >> " + system_file) conn.run("echo 'export PATH=$PATH:$JAVA_HOME/bin' >> " + system_file) # 使环境变量生效 conn.run('source ' + system_file)
这里还有个小窍门, linux 那么多配置文件如 /etc/profile , /etc/bashrc , 究竟应该写到哪个文件? 查了一下资料, /etc/profile 只在用户第一次登录的时候起作用, 所以应该写到 /etc/bashrc 里.
思路: 生成新用户很简单, 需要考虑的是给它设定密码. 考察了一下linux上生成密码的方式, 最后决定用 pwgen . 用法:
# 生成10个字符的密码, 个数为1
sudo apt install pwgen
pwgen -s 10 1
代码大概长这样:
def add_user(self, username): """ 增加用户, 并设置密码. :param username: 用户名 :return: 登录密码 """ if self._is_user_exist(username): title('用户 ' + username + ' 已经存在') fab_utils.close_connection(self.conn) return None # 生成用户. 该命令只在centos 测试过 fab_utils.run(self.conn, 'adduser ' + username) # 生成10位密码 cmd = 'pwgen -s 10 1' password = fab_utils.run(self.conn, cmd) # 更改密码 fab_utils.yum_passwd(self.conn, username, password) # 退出登录 fab_utils.close_connection(self.conn) # 登录进行测试 self.verify_login(username, self.host, password) return password
项目地址: https://gitee.com/xiaofeipapa/fabric-tutorial
请看 setup_java.py 文件
通过这么多方法和整个案例的展示, 你应该掌握了fabric 90%的用法, 剩下的就靠你自己探索了. 希望你用fabric玩得愉快~
记得给个star和赞啊, 少年~
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。