Fabric 简介#

文档见:Fabric

安装:

pip install fabric2 -i https://pypi.tuna.tsinghua.edu.cn/simple

Fabric 是高级的 Python(2.7,3.4+)库,旨在通过 SSH 远程执行 shell 命令,并返回有用的 Python 对象。它基于 Invoke(子进程命令执行和命令行功能)和 Paramiko(SSH 协议实现),扩展了它们的 API 以互补彼此并提供额外的功能。

通过 Connectionrun 运行命令#

Fabric 最基本的用途是通过 SSH 在远程系统上执行 shell 命令,然后(可选地)查询结果。默认情况下,远程程序的输出直接打印到您的终端,并被捕获。

Connection 代表 SSH 连接,并提供 Fabric API 的核心,比如 run。连接对象至少需要一个主机名才能成功创建,并且可以通过用户名和/或端口号进一步参数化。你可以通过 args/kwargs 显式地给出这些参数:

from fabric import Connection

Connection(host='web1', user='deploy', port=2202)
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
Cell In[1], line 1
----> 1 from fabric import Connection
      3 Connection(host='web1', user='deploy', port=2202)

ModuleNotFoundError: No module named 'fabric'

或者通过在 host 参数中填充 [user@]host[:port] 字符串(尽管这纯粹是为了方便;无论何时出现歧义,都要使用 kwargs!):

Connection('deploy@web1:2202')
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[2], line 1
----> 1 Connection('deploy@web1:2202')

NameError: name 'Connection' is not defined

Connection 对象的方法(如 run)通常返回 invoke.runners.Result (或其子类)的实例,暴露了上面看到的各种细节:请求了什么,远程操作发生时发生了什么,以及最终结果是什么。

备注

通过使用 connect_kwargs 参数,可以将许多低级 SSH 连接参数(如私钥和超时)直接提供给 SSH 后端。

示例:

>>> from fabric import Connection
>>> c = Connection('web1')
>>> result = c.run('uname -s')
Linux
>>> result.stdout.strip() == 'Linux'
True
>>> result.exited
0
>>> result.ok
True
>>> result.command
'uname -s'
>>> result.connection
<Connection host=web1>
>>> result.connection.host
'web1'

通过自动响应获得超级用户特权#

需要以远程系统的超级用户身份运行程序?您可以通过 run 调用 sudo 程序,并且(如果您的远程系统没有配置无密码 sudo)手动响应密码提示,如下所示。(注意需要请求一个远程伪终端;否则,大多数 sudo 实现在密码提示时都会变得暴躁。)

>>> from fabric import Connection
>>> c = Connection('db1')
>>> c.run('sudo useradd mydbuser', pty=True)
[sudo] password:
<Result cmd='sudo useradd mydbuser' exited=0>
>>> c.run('id -u mydbuser')
1001
<Result cmd='id -u mydbuser' exited=0>

每次手工输入密码会让人老去;值得庆幸的是,Invoke 强大的命令执行功能包括使用预定义输入自动响应程序输出的能力。我们可以在 sudo 中使用这个:

>>> from invoke import Responder
>>> from fabric import Connection
>>> c = Connection('host')
>>> user = "ai"
>>> sudopass = Responder(
    pattern=f'\[sudo\] password for {user}:',
    response='mypassword\n',
)
>>> c.run('sudo whoami', pty=True, watchers=[sudopass])
[sudo] password for ai:
root
<Result cmd='sudo whoami' exited=0>

很难在代码片段中显示,但是当上面的代码被执行时,用户不需要输入任何东西,密码被自动发送到远程程序。

sudo 辅助函数#

使用 watchers/responders 在这里工作得很好,但每次都需要设置很多样板文件——特别是在现实世界的用例中需要更多的工作来检测失败/错误的密码时。

为了帮助解决这个问题,Invoke 提供了 Context.sudo 方法为您处理大多数样板文件(作为 Connection 子类 Context,它自由获得此方法)sudo 不会做任何用户不能自己做的事情——但一如既往,常见的问题最好通过共享的解决方案来解决。用户需要做的就是确保 sudo.password 配置值(通过配置文件、环境变量或 --prompt-for-sudo-password)填写,Connection.sudo 处理剩下的事情。为了清晰起见,这里有一个示例,其中库/shell用户执行他们自己的基于 getpass 的密码提示:

>>> import getpass
>>> from fabric import Connection, Config
>>> sudo_pass = getpass.getpass("What's your sudo password?")
What's your sudo password?
>>> config = Config(overrides={'sudo': {'password': sudo_pass}})
>>> c = Connection('db1', config=config)
>>> c.sudo('whoami', hide='stderr')
root
<Result cmd="...whoami" exited=0>
>>> c.sudo('useradd mydbuser')
<Result cmd="...useradd mydbuser" exited=0>
>>> c.run('id -u mydbuser')
1001
<Result cmd='id -u mydbuser' exited=0>

在本例中,在运行时预先填写了 sudo 密码;在实际情况下,您也可以通过配置系统提供它(可能使用环境变量,以避免污染配置文件),或者理想情况下,使用秘密管理系统。

SSH 身份认证#

即使在“普通”OpenSSH客户端中,对远程服务器的身份验证也涉及多个潜在的秘密和配置源;Fabric 不仅支持其中的大多数,而且有更多自己的功能。

备注

由于 Fabric 本身试图不重新发明太多的 Paramiko 功能,所以大多数时候配置身份验证值归结为“如何为 SSHClient 设置关键字参数值”。,这又意味着在 connect_kwargs 配置子树或 Connectionconnect_kwargs 关键字参数中设置值。

私钥文件#

存储在磁盘上的私钥可能是 SSH 最常见的验证机制。Fabric 提供了多种方法来配置要使用的路径,其中大多数最终合并成一个路径列表交给 SSHClient.connect(key_filename=[...]),顺序如下:

  • 如果在 Connectionconnect_kwargs 参数中存在 key_filename 密钥,则它们首先出现在列表中。(这基本上是非cli用户的“运行时”选项。)

  • 配置设置 connect_kwargs.key_filename 可以通过多种方式设置(根据配置文档),包括通过 --identity CLI标志(设置配置的覆盖级别;因此,当使用此标志时,来自其他配置源的关键文件名值将被覆盖。)这个值在整个列表中排名第二。

  • 使用带有 IdentityFile 指令的 ssh_config 文件可以让你与其他 SSH 客户端共享配置;这些值排在最后。

加密密码#

如果您的私钥文件是通过密码短语保护的,它可以通过几种方式提供:

  • connect_kwargs.passphrase 配置选项是提供要自动使用的密码短语的最直接方法。

    备注

    对这种类型的数据使用实际的磁盘配置文件并不总是明智的,但请记住,配置系统 能够从其他来源加载数据,例如 shell 环境甚至任意远程数据库。

  • 如果你喜欢在运行时手动输入密码,可以使用命令行选项 --prompt-for-passphrase,这让 Fabric 在进程开始时以交互方式提示用户,并将输入的值存储在 connect_kwargs.passphrase 中(在 overrides 级别)。

私钥对象#

实例化自己的 PKey 对象并将其放入 connect_kwargs.pkey 中。就是这样!如果要加载的 key 内容(这些类可以从文件路径或字符串中加载)是加密的,那么就需要负责处理密码。

SSH 代理#

默认情况下(类似于 OpenSSH 的行为方式)Paramiko 将尝试连接到正在运行的 SSH 代理(Unix 风格,例如 live SSH_AUTH_SOCK,或者 Windows 上的 Pageant)。这可以通过设置 connect_kwargs.allow_agentFalse 来禁用。

密码#

密码验证相对简单:

  • 可以直接配置 connect_kwargs.password

  • 如果您希望在会话开始时提示输入,请指定 --prompt-for-login-password

传输文件#

除了 shell 命令执行,SSH 连接的另一个常见用途是文件传输;Connection.putConnection.get 可以满足这种需求。例如,假设想要上传文件,可以:

>>> from fabric import Connection
>>> result = Connection('web1').put('myfiles.tgz', remote='/opt/mydata/')
>>> print("Uploaded {0.local} to {0.remote}".format(result))
Uploaded /local/myfiles.tgz to /opt/mydata/

这些方法在参数求值方面通常遵循 cpscp/sftp 的行为——例如,在上面的代码片段中,省略了远程路径参数的 filename 部分。

详细示例:

from fabric import Connection
from fabric import Connection, Config

host = "10.16.11.77"
user = 'ai'
connect_kwargs = {
    # "key_filename": "C:/Users/lxw/.ssh/known_hosts",
    "password": "123456"
}
file_name = "data"
root_dir = "/media/pc/data/lxw/ai/tasks"
with Connection(
    host=host, user=user,
    connect_kwargs=connect_kwargs
) as ctx:
    ctx.run("uname -a") # 显示系统内核
    # ctx.put("fabfile.py", "/media/pc/data/lxw/ai/tasks") # 上传文件
    ctx.run(f"ls {root_dir}", pty=True)
    # tar -xvf code.tar -C /home/abc/code # 解压
    # tar -zcvf 压缩文件名.tar.gz 被压缩文件名
    ctx.run(f"tar -cvf {root_dir}/{file_name}.tar {root_dir}/data/", pty=True) # 压缩文件
    ctx.get(f"{root_dir}/{file_name}.tar") # 下载文件

多重作用#

一行程序是很好的例子,但并不总是实际的用例——通常需要多个步骤才能做任何有趣的事情。在最基本的层面上,你可以通过多次调用 Connection 方法来实现:

from fabric import Connection
c = Connection('web1')
c.put('myfiles.tgz', '/opt/mydata')
c.run('tar -C /opt/mydata -xzvf /opt/mydata/myfiles.tgz')

您可以(但不必)将这些代码块转换为函数,并使用调用方的 Connection 对象进行参数化,以鼓励重用:

def upload_and_unpack(c):
    c.put('myfiles.tgz', '/opt/mydata')
    c.run('tar -C /opt/mydata -xzvf /opt/mydata/myfiles.tgz')

正如您将在下面看到的,这样的函数也可以交给其他 API 方法来支持更复杂的用例。

多服务器#

大多数实际用例涉及在多个服务器上执行操作。直接的方法可以是迭代 Connection 参数的列表或元组(或 Connection 对象本身,可能通过 map):

>>> from fabric import Connection
>>> for host in ('web1', 'web2', 'mac1'):
        result = Connection(host).run('uname -s')
        print("{}: {}".format(host, result.stdout.strip()))


web1: Linux
web2: Linux
mac1: Darwin

这种方法是有效的,但是随着用例变得越来越复杂,将主机集合视为单个对象可能会很有用。输入 Group,一个包装一个或多个 Connection 对象并提供类似 API 的类;具体来说,您需要使用它的具体子类之一,如 SerialGroupThreadingGroup

前面的例子,使用 Group(特别是 SerialGroup),看起来像这样:

>>> from fabric import SerialGroup as Group
>>> results = Group('web1', 'web2', 'mac1').run('uname -s')
>>> print(results)
<GroupResult: {
    <Connection 'web1'>: <CommandResult 'uname -s'>,
    <Connection 'web2'>: <CommandResult 'uname -s'>,
    <Connection 'mac1'>: <CommandResult 'uname -s'>,
}>
>>> for connection, result in results.items():
        print("{0.host}: {1.stdout}".format(connection, result))


web1: Linux
web2: Linux
mac1: Darwin

Connection 方法返回单个 Result 对象(例如 fabric.runners.Result),Group 方法返回类似字典的 GroupResult 对象,提供对单个连接结果以及整个运行的元数据的访问。

当组中的任何单独的连接遇到错误时,GroupResult 会被简单地封装在抛出的 GroupException 中。因此,聚合行为类似于单个 Connection 方法,成功时返回值,失败时抛出异常。

综合范例#

最后,我们到达了最实际的用例:您有一组命令和/或文件传输,并且希望将其应用于多个服务器。你可以使用多个 Group 方法调用来做到这一点:

from fabric import SerialGroup as Group
pool = Group('web1', 'web2', 'web3')
pool.put('myfiles.tgz', '/opt/mydata')
pool.run('tar -C /opt/mydata -xzvf /opt/mydata/myfiles.tgz')

当逻辑变得必要时,这种方法就会失效——例如,如果您只想在 /opt/mydata 为空时执行上述复制和解压操作。执行这种检查需要在每个服务器上执行。

你可以通过使用 Connection 对象的可迭代对象来满足这一需求(尽管这会放弃使用 Groups 的一些好处):

from fabric import Connection
for host in ('web1', 'web2', 'web3'):
    c = Connection(host)
    if c.run('test -f /opt/mydata/myfile', warn=True).failed:
        c.put('myfiles.tgz', '/opt/mydata')
        c.run('tar -C /opt/mydata -xzvf /opt/mydata/myfiles.tgz')

另外,还记得我们在前面的例子中是如何使用函数的吗?你也可以走那条路:

from fabric import SerialGroup as Group

def upload_and_unpack(c):
    if c.run('test -f /opt/mydata/myfile', warn=True).failed:
        c.put('myfiles.tgz', '/opt/mydata')
        c.run('tar -C /opt/mydata -xzvf /opt/mydata/myfiles.tgz')

for connection in Group('web1', 'web2', 'web3'):
    upload_and_unpack(connection)

这种方法唯一缺乏的便利是 Group.run 的一个有用的类比——如果你想跟踪所有的 upload_and_unpack 调用的结果,你必须自己做。期待未来的功能版本以了解更多!

附录:fab 命令行工具#

在 shell 中运行 Fabric 代码通常很有用,例如部署应用程序或在任意服务器上运行系统管理任务。你可以使用包含 Fabric 库代码的常规调用任务,也可以使用 Fabric 自带的“面向网络的”工具 fab

fab将 Invoke 的 CLI 机制与主机选择等特性包装在一起,使您可以在不同的服务器上快速运行任务,而无需在所有任务或类似的任务上定义 host kwargs。

对于最后一个代码示例,让我们将前面的示例改编为名为 fabfile.pyfab 任务模块:

from fabric import task

@task
def upload_and_unpack(c):
    if c.run('test -f /opt/mydata/myfile', warn=True).failed:
        c.put('myfiles.tgz', '/opt/mydata')
        c.run('tar -C /opt/mydata -xzvf /opt/mydata/myfiles.tgz')

这并不难——我们所做的只是将临时任务函数复制到一个文件中,然后在上面加上一个装饰符。task 告诉 CLI 机制在命令行上公开任务:

$ fab --list
Available tasks:

  upload_and_unpack

然后,当 fab 实际调用任务时,它知道如何将控制目标服务器的参数拼接在一起,并在每个服务器上运行一次任务。在单个服务器上运行一次任务。

$ fab -H web1 upload_and_unpack

当发生这种情况时,任务中的 c 被有效地设置为 Connection("web1") ——与前面的示例一样。类似地,你可以给多个主机,它可以多次运行任务,每次都有一个不同的 Connection 实例:

$ fab -H web1,web2,web3 upload_and_unpack

更多内容见:CLI