快速上手¶
欢迎!本教程重点介绍了 Fabric 的核心特性;要了解更多的细节,请参见内部的链接,或文档索引,其中有概念文档和 API 文档部分的链接。
关于导入的说明¶
Fabric 由几个其他库组成,并在其上提供自己的层;用户代码通常从 fabric
包中导入,但有时也可以直接从 invoke
或 paramiko
中导入
Invoke 实现了 CLI 解析、任务组织和 shell 命令执行(通用框架加上本地命令的特定实现)。
任何与远程系统无关的内容都存在于 Invoke 中,并且通常由不需要任何远程功能的程序员独立使用。
Fabric 用户将经常导入 Invoke 对象,在 Fabric 本身不需要子类化或修改 Invoke 提供的内容的情况下。
Paramiko 实现了低/中级 SSH 功能- SSH 和 SFTP 会话,密钥管理,等等。
Fabric 主要在底层使用;用户很少会直接从 Paramiko 导入。
Fabric 将其他库粘合在一起,并提供自己的高级对象,例如:
子类化 Invoke 的上下文和命令运行类,将它们包装在 paramiko 级原语中;
通过使用 Paramiko 的
ssh_config
解析机制来扩展 Invoke 的配置系统实现自己的新的高级原语,比如端口转发上下文管理器。(假以时日,它们可能会向下迁移到 Paramiko。)
通过 Connections 和 run
运行命令¶
Fabric 最基本的用途是通过 SSH 在远程系统上执行 shell 命令,然后(可选地)查询结果。默认情况下,远程程序的输出直接打印到终端,并被捕获。基本的例子:
>>> 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'
满足 .Connection
,它代表 SSH 连接,并提供 Fabric 的核心 API,如 run()
。.Connection
对象至少需要主机名才能成功创建,并且可以通过用户名和/或端口号进一步参数化。你可以通过 args/kwargs 明确地给出这些参数:
Connection(host='web1', user='deploy', port=2202)
或者将 [user@]host[:port]
字符串填充到 host
参数中(尽管这纯粹是为了方便;每当出现歧义时,总是使用 kwargs!)
Connection('deploy@web1:2202')
.Connection
对象的方法(如 ~.Connection.run
)通常返回 invoke.runners.Result
的实例(或其子类),这些实例揭示了上面所看到的各种细节:请求了什么,当远程操作发生时发生了什么,以及最终结果是什么。
备注
通过使用 connect_kwargs 参数,可以将许多较低级的 SSH 连接参数(如私钥和超时)直接提供给 SSH 后端。
超级用户特权通过自动响应¶
需要以远程系统的超级用户身份运行程序吗?您可以通过 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 强大的命令执行功能包括 auto-respond 对带有预定义输入的程序输出的能力。我们可以用这个来表示 sudo
:
>>> from invoke import Responder
>>> from fabric import Connection
>>> c = Connection('host')
>>> sudopass = Responder(
... pattern=r'\[sudo\] password:',
... response='mypassword\n',
... )
>>> c.run('sudo whoami', pty=True, watchers=[sudopass])
[sudo] password:
root
<Result cmd='sudo whoami' exited=0>
这很难在代码片段中显示出来,但当执行上述命令时,用户不需要输入任何东西;mypassword
被自动发送到远程程序。容易得多!
sudo
辅助函数¶
在这里使用 watchers/responders 很有效,但是每次都需要设置很多模板——尤其是在现实的用例中需要做更多的工作来检测失败/错误的密码时。
为了帮助实现这一点,Invoke 提供了 Context.sudo
方法,它为你处理大部分样板文件(像 Connection
的子类 Context
,它会免费获得这个方法。) sudo
不会做任何用户自己做不到的事情,但和以往一样,共同的解决方案能最好地解决共同的问题。
用户需要做的就是确保 sudo password configuration value (通过配置文件、环境变量或 --prompt-for-sudo-password
和 Connection.sudo
填写剩下的工作。为了清晰起见,这里有示例,library/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 密码;在现实环境中,你也可以通过配置系统(可能使用环境变量,以避免污染配置文件)提供它,或者理想情况下,使用秘密管理系统。
传输文件¶
除了执行 shell 命令,SSH 连接的另一个常见用途是文件传输;Connection.put
和 Connection.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/
这些方法在参数求值方面通常遵循 cp
和 scp
/sftp
的行为——例如,在上面的代码片段中,我们省略了远程路径参数的文件名部分。
多动作¶
一行程序是很好的例子,但并不总是现实的用例——一个程序通常需要多个步骤才能做任何有趣的事情。在最基本的层面上,您可以通过调用 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;具体来说,你会想要使用它的具体子类,比如 SerialGroup
或 ThreadingGroup
。
上一个例子中,使用了 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
类似于 dict
的对象,提供对每个连接结果以及整个运行的元数据的访问。
当任何个人内部连接 Group
遇到错误,被轻包装在 GroupResult
将引发 GroupException
。因此,集体行为类似于个体的 Connection
方法,成功时返回值,失败时引发异常。
把它们放在一起¶
最后,我们得出了最现实的用例:你有一堆命令和/或文件传输,你想把它应用到多个服务器上。你 可以 使用 multiple 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
为空时执行上面的 copy-and-untar 操作。执行这种检查需要在每个服务器上执行。
你可以通过使用 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 库代码的 Invoke tasks,但另一个选择是 Fabric 自己的面向网络的工具 fab
。
fab
将 Invoke 的 CLI 机制与主机选择等功能包装在一起,让您可以在不同的服务器上快速运行任务,而不必在所有任务或类似的任务上定义 host
kwargs。
备注
这种模式是 Fabric 1.x 的主要 API;对于 2.0 版本来说,这只是一种方便。无论何时你的用例落在这些快捷方式之外,它应该很容易直接恢复到库API(有或没有调用的不太固执的CLI任务包装它)。
For a final code example, let’s adapt the previous example into a fab
task
module called fabfile.py
:
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')
Not hard - all we did was copy our temporary task function into a file and slap
a decorator on it. task
tells the CLI machinery to expose the
task on the command line:
$ fab --list
Available tasks:
upload_and_unpack
Then, when fab
actually invokes a task, it knows how to stitch together
arguments controlling target servers, and run the task once per server. To run
the task once on a single server:
$ fab -H web1 upload_and_unpack
When this occurs, c
inside the task is set, effectively, to
Connection("web1")
- as in earlier examples. Similarly, you can give more
than one host, which runs the task multiple times, each time with a different
Connection
instance handed in:
$ fab -H web1,web2,web3 upload_and_unpack