使用Fabric库来批量管理服务器

使用Fabric库来批量管理服务器

微信搜索 zze_coding 或扫描 👉 二维码关注我的微信公众号获取更多资源推送:

简介

fabric 是一个 Python 的库,同时它也是一个命令行工具。使用 fabric 提供的命令行工具,可以很方便地执行应用部署和系统管理等操作。
fabric 依赖于 paramiko 进行 ssh 交互,fabric 的设计思路是通过几个 API 接口来完成所有的部署,因此 fabric 对系统管理操作进行了简单的封装,比如执行命令、上传文件、并行操作和异常处理等。

安装:pip3 install fabric3

fabric 是一个 Python 库的同时它还是一个命令行工具,可通过 --help 来查看它的使用说明:

$ fab --help
Usage: fab [options] <command>[:arg1,arg2=val2,host=foo,hosts='h1;h2',...] ...

Options:
  -h, --help            show this help message and exit
  -d NAME, --display=NAME
                        print detailed info about command NAME
  -F FORMAT, --list-format=FORMAT
                        formats --list, choices: short, normal, nested
  -I, --initial-password-prompt
                        Force password prompt up-front
  --initial-sudo-password-prompt
                        Force sudo password prompt up-front
  -l, --list            print list of possible commands and exit
  ...

使用

快速入门

fabirc 对 paramiko 进行了高层次的封装,让我们能简单的调用函数实现对多台服务器的批量操作,下面就以在所有服务器中执行 echo hello fabric > /tmp/hello.txt 为例。

新建名为 test01.py 的 python 脚本文件,内容如下:

from fabric.api import *
# 主机清单
env.hosts = ['192.168.0.130', '192.168.0.109']
# SSH 端口
env.port = 22
# 用户名
env.user = 'zze'
# 密码
env.password = '123'


def hello():
    run('echo hello fabric > /tmp/hello.txt')


if __name__ == '__main__':
    execute(hello)

其执行后的输出如下:

[192.168.0.130] Executing task 'hello'
[192.168.0.130] run: echo hello fabric > /tmp/hello.txt
[192.168.0.109] Executing task 'hello'
[192.168.0.109] run: echo hello fabric > /tmp/hello.txt

从其输出的结果就可以看出,该脚本在 192.168.0.130192.168.0.109 这两台主机上执行了 echo hello fabric > /tmp/hello.txt,这里可以登入两台主机检查一下:

$ ssh zze@192.168.0.130 'cat /tmp/hello.txt'
hello fabric
$ ssh zze@192.168.0.109 'cat /tmp/hello.txt'
hello fabric

在上面的操作示例中,执行操作实际是由 fabirc 库中的 execute 函数触发的,在上面介绍内容中有提到过,fabirc 不仅仅是个 Python 库,它还提供了命令行工具,所以我们也可以通过它的命令行工具来操作 fabric 脚本,如下:

# 查看可用的命令,一个命令对应 python 脚本中的一个函数,每个函数在 fabric 也被称为 task
# -f:指定操作的 fabric 脚本;
# -l:查看指定脚本中可执行的命令(函数、task);
$ fab -f test01.py -l
Available commands:

    hello
# 执行指定的函数,可同时执行多个函数,以空格隔开即可
$ fab -f test01.py hello
[192.168.0.130] Executing task 'hello'
[192.168.0.130] run: echo hello fabric > /tmp/hello.txt
[192.168.0.109] Executing task 'hello'
[192.168.0.109] run: echo hello fabric > /tmp/hello.txt

Done.
Disconnecting from 192.168.0.130... done.
Disconnecting from 192.168.0.109... done.

当然,既然是函数,它也可以接受传参,现在来添加一个批量查看远程主机 /tmp/hello.txt 文件内容的函数到 test01.py 中:

def cat_hello(file_path='/tmp/hello.txt'):
    sudo('cat {}'.format(file_path))

执行:

# 如有多个参数,以 , 隔开即可
$ fab -f test01.py cat_hello:file_path='/tmp/hello.txt'
[192.168.0.130] Executing task 'cat_hello'
[192.168.0.130] sudo: cat /tmp/hello.txt
[192.168.0.130] out: hello fabric
[192.168.0.130] out: 

[192.168.0.109] Executing task 'cat_hello'
[192.168.0.109] sudo: cat /tmp/hello.txt
[192.168.0.109] out: hello fabric
[192.168.0.109] out: 


Done.
Disconnecting from 192.168.0.130... done.
Disconnecting from 192.168.0.109... done.

下面对上述使用到的知识点进行一下说明:

1、run 函数:用于远程执行命令;
2、sudo 函数:同样是用于远程执行命令,只是执行命令前使用 sudo 提权;
3、env 对象:保存连接相关的配置信息,比如登录用户名 env.user、密码 env.password、端口 env.port 等,如果没有指定用户名那么默认使用当前用户,端口使用 22;

命令行参数

下面再来详细了解一下 fabric 的命令行选项,其可以通过 fab --help 查看,这里列出常用选项的说明:

-l:查看 task 列表;
-f:指定 fab 的入口文件,默认是 fabfile.py;
-g:指定网关设备,比如堡垒机环境下,填写堡垒机的 IP;
-H:在命令行指定目标服务器,用逗号分隔多个服务器;
-P:以并行方式运行任务,默认为串行;
-R 指定roel(角色),以角色名区分不同业务组设备;
-t:连接超时的时间,以秒为单位;
-w:命令执行失败时的警告,默认是终止任务;
-T:设置远程主机命令执行超时时间(秒);
--user:指定登录到远程主机的用户名;
--password:指定登录到远程主机的用户的密码;
-- '<command>':要执行的命令;

直接看如下示例:

$ fab -H 192.168.0.130 --port 22 --user='zze' --password='123' -- 'cat /tmp/hello.txt'
[192.168.0.130] Executing task '<remainder>'
[192.168.0.130] run: cat /tmp/hello.txt
[192.168.0.130] out: hello fabric
[192.168.0.130] out: 


Done.
Disconnecting from 192.168.0.130... done.

env 对象详解

env 本身是一个全局唯一的字典,保存了 fabric 所有的环境配置信息,在 fabric 的实现中,env 是一个 _AttributeDict() 对象,之所以封装成 _AttributeDict() 对象,是因为它覆盖了 __getattr____setattr__,使我们可以使用 对象.属性=值 的方式,操作字典。

查看源码:

env = _AttributeDict({
    'abort_exception': None,
    'again_prompt': 'Sorry, try again.',
    'all_hosts': [],
    'combine_stderr': True,
    'colorize_errors': False,
    'command': None,
    'command_prefixes': [],
    'cwd': '',  # Must be empty string, not None, for concatenation purposes
    'dedupe_hosts': True,
    'default_port': default_port,
    'eagerly_disconnect': False,
    'echo_stdin': True,
    'effective_roles': [],
    'exclude_hosts': [],
    'gateway': None,
    'gss_auth': None,
    'gss_deleg': None,
    'gss_kex': None,
    'host': None,
    'host_string': None,
    'lcwd': '',  # Must be empty string, not None, for concatenation purposes
    'local_user': _get_system_username(),
    'output_prefix': True,
    'passwords': {},
    'path': '',
    'path_behavior': 'append',
    'port': default_port,
    'real_fabfile': None,
    'remote_interrupt': None,
    'roles': [],
    'roledefs': {},
    'shell_env': {},
    'skip_bad_hosts': False,
    'skip_unknown_tasks': False,
    'ssh_config_path': default_ssh_config_path,
    'sudo_passwords': {},
    'ok_ret_codes': [0],     # a list of return codes that indicate success
    # -S so sudo accepts passwd via stdin, -p with our known-value prompt for
    # later detection (thus %s -- gets filled with env.sudo_prompt at runtime)
    'sudo_prefix': "sudo -S -p '%(sudo_prompt)s' ",
    'sudo_prompt': 'sudo password:',
    'sudo_user': None,
    'tasks': [],
    'prompts': {},
    'use_exceptions_for': {'network': False},
    'use_shell': True,
    'use_ssh_config': False,
    'user': None,
    'version': get_version('short')
})

class _AttributeDict(dict):
    def __getattr__(self, key):
        try:
            return self[key]
        except KeyError:
            # to conform with __getattr__ spec
            raise AttributeError(key)

    def __setattr__(self, key, value):
        self[key] = value

    def first(self, *names):
        for name in names:
            value = self.get(name)
            if value:
                return value

在运行时可通过序列化的方式打印该对象的各属性值:

import json
from fabric.api import env

print(json.dumps(env, indent=3))

常用的 env 属性有如下:

env.hosts:定义目标主机,可以用 IP 或主机名表示,以 Python 的列表形式定义,如 env.hosts=["192.168.88.2", "192.168.88.3"];
env.exclude_hosts:排除指定主机,如 env.exclude_hosts=['192.168.88.2'];
env.user:定义用户名,如 env.user='root';
env.port:定义端口,默认为 22,如 env.port='22';
env.password:定义密码,如 env.password='123.com';
env.passwords:定义多个密码,不同主机对应不同密码,如:env.passwords={'root@192.168.88.2:22':'123.com', 'root@192.168.88.2:22':'123456'};
env.gateway:定义网关(中转,堡垒机)ip,如 env.gateway='192.168.88.10';
env.roledefs:定义角色组,比如 web 组合 db 组主机区分开来 env_roledefs={'webserver':['192.168.88.2', '192.168.88.3'], 'dbserver':['192.168.88.4']};
env.reject_unkown_hosts:控制未知 host 的行为,默认 True,类似于 SSH 的 StrictHostKeyChecking 的选项设置为 no,不进行公钥确认;
env.parallel:控制 task 是否并行执行,默认为 False;
env.<var>:自定义全局变量,格式:env. + '变量名', 如 env.age, env.sex 等;

针对不同主机不同密码的情况,可以使用如下的方式:

env.hosts = [
    'zze@192.168.10.201:22',
    'zze@192.168.10.202:22',
    'zze@192.168.10.203:22'
]
env.passwords = {
    'zze@192.168.10.201:22':'12301',
    'zze@192.168.10.202:22':'12302',
    'zze@192.168.10.203:22':'12303'

常用函数

run()

run 函数用于在远程服务器上执行命令,其第一个参数是要执行的命令字符串,除此之外还有一个重要的参数 pty,如果我们执行命令以后需要有一个常驻的守护进程,那么就需要设置 pty=False,避免因为 fabric 退出导致进程的退出,比如:

run('./prometheus &',pty=False)

run 函数其实还有返回值,返回对象中保存了执行命令输出的信息,同时还可通过对象的 return_code 属性判定命令的执行是否成功(成功为 0,否则为 非 0)。

def test():
    # 隐藏其它信息输出,后面会详述
    with settings(hide('everything'), warn_only=True):
        result = run('cat /tmp/hello.txt', pty=False)
        print(result) # 输出结果
        print(result.return_code) # 是否成功

该 task 的执行结果如下:

[192.168.0.130] Executing task 'start_nginx'
hello fabric
0

Done.
Disconnecting from 192.168.0.130... done.

sudo()

使用同 run 函数,只是它会使用 sudo 提权后再进行操作。

local()

用于在本地执行命令,local 是对 Python 的 subprocess 模块的封装,更复杂的功能可以直接使用 subprocess 模块,包含 capture 参数,默认为 False,表示 subprocess 输出的信息进行显示,如果不想显示,那么指定 capture=True 即可。

def test():
    result = local('hostname', capture=True)
    print(result)  # 命令的输出结果
    print(result.failed)  # 是否失败
    print(result.succeeded)  # 是否成功

该 task 的执行结果如下:

[192.168.0.130] Executing task 'test'
[localhost] local: hostname
zbook
False
True

get()

从远程服务器上获取文件,通过 remote_path 参数声明从何处下载(支持通配符),通过 local_path 表示下载到何处。

get(remote_path='/etc/passwd',local_path='./passwd')

put()

将本地的文件上传到远程服务器,参数与 get 相似,此外,还可以通过 mode 参数执行远程文件的权限配置。

put(remote_path='~', local_path='./test01.py', mode='644') 

reboot()

重启远程服务器,可以通过 wait 参数设置等待几秒钟重启。

reboot(wait=30)

propmt()

用以在 fabric 执行任务的过程中与管理员进行交互,类似于 Python 的 input 函数,但它还提供了输入信息的校验功能。

input_num = prompt('input a number:', validate=int, default='')

confirm()

输出确认提示信息。

from fabric.contrib.console import confirm
result =confirm('确认?')

lcd()

本地切换工作目录。

# 切换目录,with 块内的操作会在 ~ 中执行
with lcd('~'):
    local('echo 111 > testlcd.txt')
    local('pwd')
# 到这一步会切换回之前的目录
local('pwd')

cd()

lcd,唯一不同的是在远程主机上切换工作目录。

# 切换目录,with 块内的操作会在 ~ 中执行
with cd('~'):
    local('echo 111 > testlcd.txt')
    local('pwd')
# 到这一步会切换回之前的目录
local('pwd')

path()

配置远程服务器 PATH 环境变量,只对当前会话有效,不会影响远程服务器的其他操作。PATH 的修改支持如下几种模式:

  • append:默认行为,将给定的路径添加到 PATH 后面;
  • prepend:将给定的路径添加到 PATH 的前面;
  • replace:替换当前环境的 PATH 变量;
# 改变的 PATH 仅在 with 块内生效
with path('/tmp','prepend'):
    run("echo $PATH")
run("echo $PATH")

prefix()

它接受一个命令作为参数,表示在其执行每一条命令之前,都要先执行 prefix 的命令参数。

with prefix('echo 123'):
    # echo '123' && echo '456'
    run('echo 456')
    # echo '123' && echo '789'
    run('echo 789')

shell_env()

设置临时的环境变量。

with shell_env(HTTP_PROXY='192.168.0.138:41091'):
    # export HTTP_PROXY="192.168.0.138:41091" && echo $HTTP_PROXY"
    run('echo $HTTP_PROXY')
run('echo $HTTP_PROXY')

settings()

通用配置,用于临时覆盖 env 变量。

with settings(user='zze'):  # 临时修改用户名为 zze
    run('whoami')
run('whoami')

hide()

用于隐藏指定类型的输出信息,hide 定义的可选类型有如下 7 种:

  • status:状态信息,如服务器断开链接,用户使用 ctrl+C 等,如果 fabric 顺利执行,不会有状态信息;
  • aborts:终止信息,一般将 fabric 当作库使用的时候需要关闭;
  • warnings:警告信息,如 grep 的字符串不在文件中;
  • running:fabric 运行过程中的数据;
  • stdout:执行 shell 命令的标准输出;
  • stderr:执行 shell 命令的错误输出;
  • user:用户输出,类似于 Python 中的 print 函数;

为方便使用,fabric 对以上 7 种类型做了进一步的封装:

  • output:包含 stdoutstderr
  • everything:包含 stdoutstderrwarningsrunninguser
  • commands:包含 stdoutrunning
with settings(hide('everything'), warn_only=True):
    run('cat hello.txt')

show()

hide 相反,表示显示指定类型的输出。

with settings(show('commands'), warn_only=True):
    run('echo hello')

quiet()

隐藏全部输出,仅在执行错误的时候发出告警信息,功能等同于 with settings(hide('everything'),warn_only=True)

with settings(quiet(),warn_only=True):
    run("echo hello")

装饰器

fabric 提供的装饰器用来控制如何执行操作、在哪些服务器上执行这些操作,主要有如下几个装饰器:

  • hosts:定制执行 task 的服务器列表;
  • roles:定义执行 task 的 role 列表;
  • parallel:并行执行 task;
  • serial:串行执行 task;
  • task:定义一个 task;
  • runs_once:该 task 只执行一次;

@task

task 装饰器用来标识一个函数为 task,表示该函数可以在远程主机上执行,如果使用了 task 装饰器,那么每个未被 task 装饰的函数就不是 task,如果没有使用 task 装饰器,则所有函数都可以作为 task 在远程主机执行。

from fabric.api import *

env.user = 'root'
env.password = 'root1234'


@task
def hello():
    run('echo hello')


def world():
    run('echo world')

使用 fab 命令查看可执行的 task 如下:

$ fab -f test02.py -l
Available commands:

    hello

@host

fabric 提供了非常灵活的方式让我们可以指定对哪些远程服务器执行操作,根据我们前面的知识,我们已经知道有如下两种方式:

  • 通过 env.hosts 来定义要执行 task 的主机列表,指定 host 时,可以同时指定用户名和端口号:username@hostname:port
  • 在使用 fab 命令的时候使用 -H 参数,或在 task 名称后面定义 hosts 变量,如 fab mytask:hosts="host1;host2"

除了上述两种方式外,我们还可以使用 host 装饰器来指定某 task 仅能在某些主机上执行,如下:

from fabric.api import *

env.hosts = [
    'root@192.168.0.130:22',
    'root@192.168.0.109:22',
]
env.passwords = {
    'root@192.168.0.130:22': '123',
    'root@192.168.0.109:22': '123',
}

@hosts('root@192.168.0.130:22')
@task
def hello():
    run('hostname')

@task
def world():
    run('hostname')

此时执行 hello task 则仅会在 192.168.0.130 主机上执行。

如果去掉上面的 @host 装饰器,此时也可以使用 fab 命令达到相同的效果,如下:

fab -f test03.py hello:hosts="root@192.168.0.130"
[root@192.168.0.130] Executing task 'hello'
[root@192.168.0.130] run: hostname
[root@192.168.0.130] out: w130
[root@192.168.0.130] out: 


Done.
Disconnecting from root@192.168.0.130... done.

@role

role 是对服务器进行分类的手段,通过 role 装饰器可以定义服务器的角色,以便对具有相同功能的服务器批量执行不同的操作。

from fabric.api import *

env.hosts = [
    'gyadmin@192.168.0.130:22',
    'gyadmin@192.168.0.109:22',
]
env.passwords = {
    'gyadmin@192.168.0.130:22': 'Speakin@2020#spk',
    'gyadmin@192.168.0.109:22': 'Speakin@2020#spk',
}

# 定义 Role
env.roledefs = {
    # 定义一个名称为 web 的 Role
    'web': ['gyadmin@192.168.0.130:22',],
    # 定义一个名称为 db 的 Role
    'db': ['gyadmin@192.168.0.109:22', ]
}

# 只对 Role 为 web 的主机进行操作
@roles('web')
@task
def hello_web():
    run('echo web')

# 只对 Role 为 db 的主机进行操作
@roles('db')
@task
def hello_db():
    run('echo db')

@parallel

通知 fabric 并行执行 task,它接受一个 pool_size 作为参数(默认为 0),表示可以有几个任务并行执行。

@parallel(pool_size=3)
@task
def hello():
    run('echo hello')

@serial

强制被装饰的 task 串行执行,使用该装饰器时优先级最高,即便是指定了并发执行的参数。

@task
@serial
def hello():
    run('echo hello')

@runs_once

让被装饰的 task 只执行一次,防止指定的 task 被多次调用。

@runs_once
def get_name():
    name = prompt('please input a your name:')
    return name


@task
def print_name():
    name = get_name()
    run('echo ' + name)

补充

utils 模块

fabric.utils 模块包含一些辅助用的功能函数,常用的函数如下:

  • abort:终止函数执行,打印错误信息到 stderr,并且以退出码 1 退出;
  • warn:输出警告信息,但是不会终止函数的执行;
  • puts:打印输出,类似于 Python 中的 print 函数;
@task
@runs_once
def test_utils():
    abort('----->abort')  # 执行到这里时,直接退出
    warn('----->warn')  # 会发出提示信息,不会退出
    puts('----->puts')  # 会打印括号中的信息

带颜色的输出

fabric 为了让输出日志更具有可读性,对命令行终端的颜色输出进行了封装,使用 print 打印带有不同颜色的文本,这些颜色包含在 fabric.color s中。像 warnputs 打印输出的内容也可以直接渲染颜色,常用函数有如下:

  • blue(text,blod=False):蓝色;
  • cyan(text,blod=False):淡蓝色;
  • green(text,blod=False):绿色;
  • magenta(text,blod=False):紫色;
  • red(text,blod=False):红色;
  • white(text,blod=False):白色;
  • yellow(text,blod=False):黄色;
from fabric.colors import *
def test_color():
    print(blue('蓝色',bold=True))
    print(cyan('淡蓝色',bold=False))
    print(green('绿色',bold=True))
    print(magenta('紫色',bold=True))
    print(red('红色',bold=False))
    print(white('白色',bold=True))
    print(yellow('黄色',bold=False))

参考:

Copyright: 采用 知识共享署名4.0 国际许可协议进行许可

Links: https://www.zze.xyz/archives/python-fabric.html

Buy me a cup of coffee ☕.