以deploy用户身份通过Fabric激活虚拟环境

130

我想在本地运行我的fabric脚本,脚本将登录到我的服务器,切换到deploy用户,激活项目的虚拟环境,然后进入项目目录并执行git pull。

def git_pull():
    sudo('su deploy')
    # here i need to switch to the virtualenv
    run('git pull')

通常我使用virtualenvwrapper的workon命令,该命令会导入activate文件并执行postactivate文件以进入项目文件夹。在这种情况下,因为fabric是在shell内部运行,所以控制权会转移到fabric,所以我不能使用bash的source内置命令来执行"$source ~/.virtualenv/myvenv/bin/activate"。

有人能提供如何实现这一点的具体示例和说明吗?


1
出于好奇,为什么你不使用workon作为前缀呢? - Daniel C. Sobral
10个回答

138

关于bitprophet的更新,使用Fabric 1.0,可以利用prefix()和自己的上下文管理器。

from __future__ import with_statement
from fabric.api import *
from contextlib import contextmanager as _contextmanager

env.hosts = ['servername']
env.user = 'deploy'
env.keyfile = ['$HOME/.ssh/deploy_rsa']
env.directory = '/path/to/virtualenvs/project'
env.activate = 'source /path/to/virtualenvs/project/bin/activate'

@_contextmanager
def virtualenv():
    with cd(env.directory):
        with prefix(env.activate):
            yield

def deploy():
    with virtualenv():
        run('pip freeze')

@simon,通过编写自己的前缀方法调用.bashrc并将前缀和命令都包含在bash的-c参数中来实现。请参见下面链接:https://dev59.com/EXM_5IYBdhLWcg3w4nnb#18397479 - Dave
5
但是sh不知道source是什么意思。我们应该如何告诉Fabric使用bash? - Pierre de LESPINAY
2
@PierredeLESPINAY,你可以使用“.”代替“source”。 - katy lavallee
当你在prefix()中完全指定路径来激活时,为什么要使用cd() - Nick T
@NickT 因为 prefix() 似乎不能切换到那个目录 - 请参考这些文档,它们可以做到同样的效果。我们想要 cd 到那里,这样当我们 yield 执行其他命令(例如我的示例中的 pip freeze)时,这些命令可以相对于该目录执行。 - nh2

96

现在你可以像我一样做,这种方法有些笨拙但是完全可行*(这种用法假设你正在使用virtualenvwrapper -- 你应该使用它 -- 如果没有,你可以很容易地替换为更长的“source”调用):

def task():
    workon = 'workon myvenv && '
    run(workon + 'git pull')
    run(workon + 'do other stuff, etc')

自版本1.0以来,Fabric有一个prefix上下文管理器,它使用了这种技术,因此您可以例如:

def task():
    with prefix('workon myvenv'):
        run('git pull')
        run('do other stuff, etc')
*当使用command1 && command2的方法时,可能会出现问题,例如当command1失败时(command2将不会运行),或者如果command1没有被正确转义并包含特殊的shell字符等情况。*

7
但是 workon 命令在 sh 中无法识别。我们该如何告诉 Fabric 使用 bash 呢? - Pierre de LESPINAY
18
我认为你应该只使用source venv/bin/activate。这更简单并且可以直接使用。 workon是一个额外的依赖项,即使安装了它,你也必须将其添加到.bashrc中——对于fabric部署来说过于复杂。 - Dave Halter
@PierredeLESPINAY 可以查看 https://dev59.com/6mgu5IYBdhLWcg3wgnRh 获取解决您问题的方法。 - dukebody

18

我只是使用一个简单的包装函数virtualenv(),可以代替run()函数进行调用。它不使用cd上下文管理器,因此可以使用相对路径。

def virtualenv(command):
    """
    Run a command in the virtualenv. This prefixes the command with the source
    command.
    Usage:
        virtualenv('pip install django')
    """
    source = 'source %(project_directory)s/bin/activate && ' % env
    run(source + command)

9

virtualenvwrapper 可以让这个过程更简单一些。

  1. Using @nh2's approach (this approach also works when using local, but only for virtualenvwrapper installations where workon is in $PATH, in other words -- Windows)

    from contextlib import contextmanager
    from fabric.api import prefix
    
    @contextmanager
    def virtualenv():
        with prefix("workon env1"):
            yield
    
    def deploy():
        with virtualenv():
            run("pip freeze > requirements.txt")
    
  2. Or deploy your fab file and run this locally. This setup lets you activate the virtualenv for local or remote commands. This approach is powerful because it works around local's inability to run .bashrc using bash -l:

    @contextmanager
    def local_prefix(shell, prefix):
        def local_call(command):
            return local("%(sh)s \"%(pre)s && %(cmd)s\"" % 
                {"sh": shell, "pre": prefix, "cmd": command})
        yield local_prefix
    
    def write_requirements(shell="/bin/bash -lic", env="env1"):
        with local_prefix(shell, "workon %s" % env) as local:
            local("pip freeze > requirements.txt")
    
    write_requirements()  # locally
    run("fab write_requirements")
    

感谢总结nh2的答案,虚拟环境上下文管理器声明可以在Python 2.6+中完成5行代码,但不能保证'workon'别名始终正确导入,使用`source .../activate'命令更加可靠。 - Alex Volkov

8
这是我关于如何在本地部署中使用virtualenv的方法。
使用fabric的path()上下文管理器,可以使用来自virtualenv的二进制文件运行pippython
from fabric.api import lcd, local, path

project_dir = '/www/my_project/sms/'
env_bin_dir = project_dir + '../env/bin/'

def deploy():
    with lcd(project_dir):
        local('git pull origin')
        local('git checkout -f')
        with path(env_bin_dir, behavior='prepend'):
            local('pip freeze')
            local('pip install -r requirements/staging.txt')
            local('./manage.py migrate') # Django related

            # Note: previous line is the same as:
            local('python manage.py migrate')

            # Using next line, you can make sure that python 
            # from virtualenv directory is used:
            local('which python')

我非常喜欢这个 -- 我没有看到任何明显的缺点,而且它非常干净。谢谢 :) - simon
这仍然是最好的和最干净的答案。 - n1_

4
感谢所有发布答案的人,我想再添加一种解决方案。有一个模块fabric-virtualenv,可以提供与相同代码相同的功能:
>>> from fabvenv import virtualenv
>>> with virtualenv('/home/me/venv/'):
...     run('python foo')

fabric-virtualenv使用`fabric.context_managers.prefix`,这可能是一个不错的方法 :)

有趣,但我不喜欢没有 SCM/问题跟踪器链接的事实。一个只在 PYPI 上发布而没有源代码和问题跟踪器链接的软件包并不能激发太多信任... 但这很容易解决。 - sorin

2

如果您想在环境中安装软件包或根据环境中已有的软件包运行命令,我找到了这个技巧来解决我的问题。与其编写复杂的fabric方法或安装新的操作系统软件包,不如使用这种方法:

/path/to/virtualenv/bin/python manage.py migrate/runserver/makemigrations  # for running commands under virtualenv

local("/home/user/env/bin/python manage.py migrate")    # fabric command


/path/to/virtualenv/bin/pip install -r requirements.txt   # installing/upgrading virtualenv

local("/home/user/env/bin/pip install -r requirements.txt")  #  fabric command

这样做的好处是您可能不需要激活环境,但您可以在环境下执行命令。


1

这种方法对我很有效,你也可以尝试应用。

from fabric.api import run 
# ... other code...
def install_pip_requirements():
    run("/bin/bash -l -c 'source venv/bin/activate' "
        "&& pip install -r requirements.txt "
        "&& /bin/bash -l -c 'deactivate'")

假设venv是您的虚拟环境目录,请在适当位置添加此方法。

1
这是一个装饰器的代码,将在任何运行/sudo调用中使用虚拟环境:
# This is the bash code to update the $PATH as activate does
UPDATE_PYTHON_PATH = r'PATH="{}:$PATH"'.format(VIRTUAL_ENV_BIN_DIR)

def with_venv(func, *args, **kwargs):
  "Use Virtual Environment for the command"

  def wrapped(*args, **kwargs):
    with prefix(UPDATE_PYTHON_PATH):
      return func(*args, **kwargs)

  wrapped.__name__ = func.__name__
  wrapped.__doc__ = func.__doc__
  return wrapped

然后使用装饰器,注意装饰器的顺序很重要:

@task
@with_venv
def which_python():
  "Gets which python is being used"
  run("which python")

1

我使用带有插件pyenv-virtualenvwrapper的pyenv。 我尝试过workon,但没有成功,相反我使用了这个(fabric 2.5):

with c.prefix('source /home/mirek/.virtualenvs/%s/bin/activate' % PROJECT):
    with c.prefix('cd /home/mirek/%s/%s' % (PROJECT, PROJECT)):
        c.run('python manage.py ....')

对于 git pull 来说,代理转发很好用,即使用 ssh -A .. 或者更好的方式是在 ~/.ssh/config 中添加如下内容:
Host forpsi
    HostName xx.xx.xx.xx
    IdentityFile /home/mirek/.ssh/id_ed25519_xxx
    ForwardAgent yes

如果您在开发机器上有代理中的私钥(经过ssh-add或者在~/.ssh/config中设置了AddKeysToAgent yes之后,执行git push),那么git pull就不会要求输入密钥密码。

完美地与Pyenv配合使用。谢谢。 - Houman

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