如何在Fabric文件中设置目标主机

109

我想使用Fabric将我的Web应用程序代码部署到开发、演示和生产服务器。我的fabfile:

def deploy_2_dev():
  deploy('dev')

def deploy_2_staging():
  deploy('staging')

def deploy_2_prod():
  deploy('prod')

def deploy(server):
  print 'env.hosts:', env.hosts
  env.hosts = [server]
  print 'env.hosts:', env.hosts

样例输出:

host:folder user$ fab deploy_2_dev
env.hosts: []
env.hosts: ['dev']
No hosts found. Please specify (single) host string for connection:
当我创建一个set_hosts()任务,如Fabric文档中所示,env.hosts会被正确设置。但这不是一个可行的选项,装饰器也不是。通过命令行传递hosts最终会导致一些调用fabfile的shell脚本,我希望有一个单一的工具来正确执行此操作。
Fabric文档中说'env.hosts只是一个Python列表对象'。但根据我的观察,这显然是不真实的。
请问有人能解释一下这里发生了什么吗?我该如何设置要部署到的主机?

我有同样的问题,你找到了什么解决办法吗? - serverhorror
要对多个服务器运行相同的任务,请使用"fab -H staging-server,production-server deploy"。更多内容请参见我的回答:https://dev59.com/OHE95IYBdhLWcg3wY9H6#21458231 - Brad Parks
尝试这个:http://docs.fabfile.org/en/1.13/usage/env.html#passwords - Dhruv Aggarwal
此答案不适用于 Fabric 2+。如果有更熟悉 Stackoverflow 约定的人可以编辑问题或问题标题以引用 Fabric 1,那可能会有所帮助。 - Jonathan Berger
15个回答

130

我通过为每个环境声明一个实际的函数来实现这一点。例如:

def test():
    env.user = 'testuser'
    env.hosts = ['test.server.com']

def prod():
    env.user = 'produser'
    env.hosts = ['prod.server.com']

def deploy():
    ...

使用上述函数,我将输入以下内容以部署到我的测试环境:

fab test deploy

...并将以下内容部署到生产环境:

fab prod deploy

这种方法的好处是可以在任何 fab 函数中使用 testprod,而不仅仅是在 deploy 中。这非常有用。


11
由于fabric存在一个bug(http://code.fabfile.org/issues/show/138#change-1497),最好在主机字符串中包含用户(如produser@prod.server.com),而不是设置env.user。 - Mikhail Korobov
1
我曾经遇到过同样的问题,这似乎是最好的解决方案。我在一个YAML文件中定义了主机、用户和许多其他设置,该文件由dev()和prod()函数加载。(这样我就可以为类似的项目重用同一个Fabric脚本。) - Christian Davén
@MikhailKorobov:当我按照你的链接访问时,我看到了“_Welcome to nginx!_”。所有对code.fabfile.org域名的请求都会得到这样的响应。 - Tadeck
2
很遗憾,看起来这个方法不再可行了——fabric 将无法运行未定义 env.hosts 的任务,并且不会像 fab A B C 那样运行函数,除非它们被定义为任务。 - dakota
http://docs.fabfile.org/en/1.10/usage/execution.html#the-alternate-approach 声称它仍然可用。 - AlexChaffee
显示剩余2条评论

77

使用roledefs

from fabric.api import env, run

env.roledefs = {
    'test': ['localhost'],
    'dev': ['user@dev.example.com'],
    'staging': ['user@staging.example.com'],
    'production': ['user@production.example.com']
} 

def deploy():
    run('echo test')

使用-R选项选择角色:

$ fab -R test deploy
[localhost] Executing task 'deploy'
...

7
如果任务总是在相同的角色上运行,您可以在任务上使用@roles()装饰器。 - Tom
2
听起来,roledefs 比将它们定义在单独的任务中更好。 - Ehtesh Choudhury
有人知道我如何在roledef中为提供的用户名包含密码吗?进一步的字典条目'password': 'some_password'似乎被忽略并导致运行时提示。 - Dirk
@Dirk,你可以使用env.passwords,它是一个字典,包含用户+主机+端口作为键和密码作为值。例如,env.passwords = {'user@host:22':'password'}。 - Jonathan

49

以下是 serverhorror的回答 的简化版本:

from fabric.api import settings

def mystuff():
    with settings(host_string='192.0.2.78'):
        run("hostname -f")

2
根据文档,settings上下文管理器用于覆盖env变量,而不是最初设置它们。我认为像thomie建议的那样使用roledefs更适合定义像stage、dev和test这样的主机。 - Tony

21

我自己也曾经卡在这个问题上,但最终找出问题所在。你简单地说就是不能从任务内部设置env.hosts配置。每个任务会针对指定的每个主机执行N次,因此该设置基本上是在任务范围之外。

看着你上面的代码,你可以简单地做到这一点:

@hosts('dev')
def deploy_dev():
    deploy()

@hosts('staging')
def deploy_staging():
    deploy()

def deploy():
    # do stuff...

这似乎可以实现你想要的功能。

或者你可以在全局范围内编写一些自定义代码,手动解析参数,并在任务函数被定义之前设置 env.hosts。出于一些原因,这实际上是我设置我的环境的方式。


找到了一种方法:from fabric.api import env; env.host_string = "dev" - Roman

18
自fab 1.5版本以来,这是一种记录的动态设置主机的方法。

http://docs.fabfile.org/en/1.7/usage/execution.html#dynamic-hosts

以下是文档中的引用。

使用动态设置的主机列表执行

Fabric 的一个常见的中高级用例是在运行时参数化查找目标主机列表(当使用角色不足时)。使用 execute 可以使这变得非常简单,如下所示:

from fabric.api import run, execute, task

# For example, code talking to an HTTP API, or a database, or ...
from mylib import external_datastore

# This is the actual algorithm involved. It does not care about host
# lists at all.
def do_work():
    run("something interesting on a host")

# This is the user-facing task invoked on the command line.
@task
def deploy(lookup_param):
    # This is the magic you don't get with @hosts or @roles.
    # Even lazy-loading roles require you to declare available roles
    # beforehand. Here, the sky is the limit.
    host_list = external_datastore.query(lookup_param)
    # Put this dynamically generated host list together with the work to be
    # done.
    execute(do_work, hosts=host_list)

3
这页底部有很多非常好的答案。 - Matt Montag

10

与其他答案相反,修改任务中的env环境变量是可能的。但是,这个env仅会用于通过fabric.tasks.execute函数执行的后续任务。

from fabric.api import task, roles, run, env
from fabric.tasks import execute

# Not a task, plain old Python to dynamically retrieve list of hosts
def get_stressors():
    hosts = []
    # logic ...
    return hosts

@task
def stress_test():
    # 1) Dynamically generate hosts/roles
    stressors = get_stressors()
    env.roledefs['stressors'] = map(lambda x: x.public_ip, stressors)

    # 2) Wrap sub-tasks you want to execute on new env in execute(...)
    execute(stress)

    # 3) Note that sub-tasks not nested in execute(...) will use original env
    clean_up()

@roles('stressors')
def stress():
    # this function will see any changes to env, as it was wrapped in execute(..)
    run('echo "Running stress test..."')
    # ...

@task
def clean_up():
    # this task will NOT see any dynamic changes to env

如果没有在execute(...)中包装子任务,则会使用模块级别的env设置或者从fab CLI传递过来的设置。


如果您想动态设置env.hosts,那么这是最佳答案。 - JahMyst

9
为了解释这为什么是个问题,命令fab使用的是fabric库来在主机列表上运行任务。如果你试图在任务内部更改主机列表,实际上就是在迭代过程中尝试更改列表。或者在没有定义主机的情况下,循环遍历一个空列表,在设置要循环遍历的列表的代码中从未执行的情况下。
使用env.host_string的方法只是解决此问题的一种方法,因为它直接指定要连接的主机。这会引起一些问题,因为如果你想要在多个主机上执行任务,就需要重新制作执行循环。
人们最简单的设置运行时主机的方法是将env填充作为一个独立的任务,该任务设置所有的主机字符串、用户等。然后他们运行部署任务。它看起来像这样:
fab production deploy

或者

fab staging deploy

测试环境和生产环境与您所提供的任务类似,但它们不会自行调用下一个任务。这样做的原因是任务必须完成并退出循环(在环境情况下为无),然后重新定义主机循环(由前面的任务定义)。


9

您需要设置host_string,例如:

from fabric.context_managers import settings as _settings

def _get_hardware_node(virtualized):
    return "localhost"

def mystuff(virtualized):
    real_host = _get_hardware_node(virtualized)
    with _settings(
        host_string=real_host):
        run("echo I run on the host %s :: `hostname -f`" % (real_host, ))

太好了。我在这里的另一个答案中发布了代码的简化版本。 - tobych

3

您需要在模块级别上修改env.hosts,而不是在任务函数内部。我也犯了同样的错误。

from fabric.api import *

def _get_hosts():
    hosts = []
    ... populate 'hosts' list ...
    return hosts

env.hosts = _get_hosts()

def your_task():
    ... your task ...

3
很简单。只需初始化env.host_string变量,所有后续命令都将在此主机上执行。
from fabric.api import env, run

env.host_string = 'user@exmaple.com'

def foo:
    run("hostname -f")

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接