Como definir hosts de destino no arquivo do Fabric

107

Quero usar o Fabric para implantar meu código de aplicativo da web em servidores de desenvolvimento, teste e produção. Meu 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

Saída de amostra:

host:folder user$ fab deploy_2_dev
env.hosts: []
env.hosts: ['dev']
No hosts found. Please specify (single) host string for connection:

Quando eu crio uma set_hosts()tarefa conforme mostrado nos documentos do Fabric , env.hosts é definido corretamente. No entanto, essa não é uma opção viável, nem um decorador. Passar hosts na linha de comando acabaria resultando em algum tipo de script de shell que chama o fabfile. Eu preferiria que uma única ferramenta fizesse o trabalho corretamente.

Ele diz na documentação do Fabric que 'env.hosts é simplesmente um objeto de lista Python'. Pelas minhas observações, isso simplesmente não é verdade.

Alguém pode explicar o que está acontecendo aqui? Como posso definir o host para implantar?

ssc
fonte
Eu tenho o mesmo problema, vocês encontraram alguma solução para isso?
Martin M.
para executar a mesma tarefa em vários servidores, use "fab -H staging-server, production-server deploy" ... mais na minha resposta abaixo: stackoverflow.com/a/21458231/26510
Brad Parks
Esta resposta não se aplica ao tecido 2+. Se alguém mais familiarizado com as convenções do Stackoverflow pudesse editar a pergunta ou o título da pergunta para se referir ao fabric 1, isso poderia ser útil.
Jonathan Berger

Respostas:

128

Faço isso declarando uma função real para cada ambiente. Por exemplo:

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

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

def deploy():
    ...

Usando as funções acima, eu digitaria o seguinte para implantar em meu ambiente de teste:

fab test deploy

... e o seguinte para implantar na produção:

fab prod deploy

O bom de fazer isso dessa maneira é que as funções teste prodpodem ser usadas antes de qualquer função fab, não apenas na implantação. É incrivelmente útil.

Zac
fonte
10
Devido a um bug no fabric ( code.fabfile.org/issues/show/138#change-1497 ), é melhor incluir o usuário na string do host (como [email protected]) em vez de definir env.user.
Mikhail Korobov
1
Tive o mesmo problema e esta parece ser a melhor solução. Eu defino os hosts, o usuário e muitas outras configurações em um arquivo YAML que é carregado pelas funções dev () e prod (). (Para que eu possa reutilizar o mesmo script de Tecido para projetos semelhantes.)
Christian Davén
@MikhailKorobov: Quando segui seu link, vi " Bem-vindo ao nginx! ". Todas as solicitações de code.fabfile.orgdomínio têm respostas assim.
Tadeck
Sim, parece que todos os bugs foram migrados para o github.
Mikhail Korobov
2
Infelizmente, parece que isso não funciona mais - o fabric não executará tarefas sem env.hosts já definido e não executará funções no fab A B Cestilo sem que sejam definidas como tarefas.
DNelson 02 de
77

Use roledefs

from fabric.api import env, run

env.roledefs = {
    'test': ['localhost'],
    'dev': ['[email protected]'],
    'staging': ['[email protected]'],
    'production': ['[email protected]']
} 

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

Escolha a função com -R:

$ fab -R test deploy
[localhost] Executing task 'deploy'
...
Thomie
fonte
7
Ou se a tarefa é sempre executada na mesma função, você pode usar o decorador @roles () na tarefa.
Tom
2
Parece que roledefs é uma solução melhor do que defini-los em tarefas separadas.
Ehtesh Choudhury
Alguém sabe como posso incluir uma senha para o nome de usuário fornecido em um roledef? Uma outra entrada do dicionário 'password': 'some_password'parece ser ignorada e leva a um prompt em tempo de execução.
Dirk
@Dirk você pode usar env.passwords que é um dicionário contendo usuário + host + porta como chave e senha como valor. Ex: env.passwords = {'user @ host: 22': 'password'}
Jonathan
49

Aqui está uma versão mais simples da resposta do serverhorror :

from fabric.api import settings

def mystuff():
    with settings(host_string='192.0.2.78'):
        run("hostname -f")
tobych
fonte
2
De acordo com os documentos , o gerenciador de contexto de configurações serve para substituir envvariáveis, não para defini-las inicialmente. Acho que usar roledefs , como sugeriu thomie, é mais apropriado para definir hosts como stage, dev e test.
Tony
21

Eu mesmo estava preso nisso, mas finalmente descobri. Você simplesmente não pode definir a configuração env.hosts de dentro de uma tarefa. Cada tarefa é executada N vezes, uma para cada Host especificado, portanto, a configuração está fundamentalmente fora do escopo da tarefa.

Olhando para o seu código acima, você poderia simplesmente fazer isso:

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

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

def deploy():
    # do stuff...

O que parece que faria o que você pretende.

Ou você pode escrever algum código personalizado no escopo global que analisa os argumentos manualmente e define env.hosts antes que sua função de tarefa seja definida. Por alguns motivos, foi assim que configurei o meu.

GoldenBoy
fonte
Encontrou uma maneira from fabric.api import env:; env.host_string = "dev"
Romano de
18

Desde o fab 1.5, esta é uma maneira documentada de configurar hosts dinamicamente.

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

Citação do doc abaixo.

Usando executar com listas de host definidas dinamicamente

Um caso de uso intermediário a avançado comum para o Fabric é parametrizar a pesquisa da lista de hosts de destino no tempo de execução (quando o uso de Funções não é suficiente). execute pode tornar isso extremamente simples, como:

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)
ja
fonte
3
+1. Muitas respostas realmente boas no final da página aqui.
Matt Montag
10

Ao contrário de algumas outras respostas, é possível modificar as envvariáveis ​​de ambiente dentro de uma tarefa. No entanto, isso envsó será usado para tarefas subsequentes executadas com a fabric.tasks.executefunção.

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

Sem envolver subtarefas execute(...), suas envconfigurações de nível de módulo ou o que for passado da fabCLI serão usados.

pztrick
fonte
Esta é a melhor resposta se você deseja definir env.hosts dinamicamente.
JahMyst
9

Você precisa dar o host_stringexemplo:

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, ))
Martin M.
fonte
Doce. Publiquei uma versão mais simples do código em outra resposta aqui.
tobych
9

Para explicar por que é um problema. O comando fab está aproveitando a estrutura da biblioteca para executar as tarefas nas listas de hosts. Se você tentar alterar a lista de hosts dentro de uma tarefa, estará essencialmente tentando alterar uma lista enquanto itera sobre ela. Ou, no caso em que você não tem hosts definidos, faça um loop em uma lista vazia onde o código onde você definiu a lista para fazer um loop nunca é executado.

O uso de env.host_string é uma solução alternativa para esse comportamento apenas porque especifica diretamente para as funções com quais hosts se conectar. Isso causa alguns problemas, pois você estará refazendo o loop de execução se quiser ter vários hosts para executar.

A maneira mais simples de as pessoas criarem a capacidade de definir hosts em tempo de execução é manter o preenchimento de env como uma tarefa distinta, que configura todas as strings de host, usuários, etc. Em seguida, eles executam a tarefa de implantação. Se parece com isso:

fab production deploy

ou

fab staging deploy

Onde a preparação e a produção são como as tarefas que você atribuiu, mas não chamam a próxima tarefa por si mesmas. A razão para funcionar assim é que a tarefa precisa terminar e sair do loop (de hosts, no caso de env Nenhum, mas é um loop de um naquele ponto), e então ter o loop encerrado os hosts (agora definidos pela tarefa anterior) novamente.

Morgan
fonte
3

Você precisa modificar env.hosts no nível do módulo, não dentro de uma função de tarefa. Eu cometi o mesmo erro.

from fabric.api import *

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

env.hosts = _get_hosts()

def your_task():
    ... your task ...
mlbright
fonte
3

É muito simples. Basta inicializar a variável env.host_string e todos os comandos a seguir serão executados neste host.

from fabric.api import env, run

env.host_string = '[email protected]'

def foo:
    run("hostname -f")
Vladimir Osintsev
fonte
3

Sou totalmente novo no fabric, mas para fazer com que o fabric execute os mesmos comandos em vários hosts (por exemplo, para implantar em vários servidores, em um comando), você pode executar:

fab -H staging-server,production-server deploy 

onde staging-server e production-server são 2 servidores nos quais você deseja executar a ação de implantação. Aqui está um fabfile.py simples que exibirá o nome do sistema operacional. Observe que o fabfile.py deve estar no mesmo diretório onde você executa o comando fab.

from fabric.api import *

def deploy():
    run('uname -s')

Isso funciona com o tecido 1.8.1, pelo menos.

Brad Parks
fonte
3

Portanto, para definir os hosts e fazer com que os comandos sejam executados em todos os hosts, você deve começar com:

def PROD():
    env.hosts = ['10.0.0.1', '10.0.0.2']

def deploy(version='0.0'):
    sudo('deploy %s' % version)

Depois de definidos, execute o comando na linha de comando:

fab PROD deploy:1.5

O que executará a tarefa de implantação em todos os servidores listados na função PROD, pois define o env.hosts antes de executar a tarefa.

Athros
fonte
Suponha que a implantação no primeiro host funcione, mas a do segundo host falhe. Como faço isso novamente apenas no segundo?
nos
2

Você pode atribuir a env.hoststringantes de executar uma subtarefa. Atribua a esta variável global em um loop se quiser iterar em vários hosts.

Infelizmente para você e para mim, o tecido não foi projetado para esse caso de uso. Verifique a mainfunção em http://github.com/bitprophet/fabric/blob/master/fabric/main.py para ver como funciona.

Andrew B.
fonte
2

Aqui está outro padrão "summersault" que permite o fab my_env_1 my_commanduso:

Com esse padrão, só temos que definir ambientes uma vez usando um dicionário. env_factorycria funções com base nos nomes-chave de ENVS. Coloquei ENVSem seu próprio diretório e arquivo secrets.config.pypara separar a configuração do código de malha.

A desvantagem é que, conforme está escrito, adicionar o @taskdecorador irá quebrá-lo .

Notas: Nós usamos em def func(k=k):vez de def func():na fábrica devido à encadernação tardia . Pegamos o módulo em execução com esta solução e corrigimos para definir a função.

secrets.config.py

ENVS = {
    'my_env_1': {
        'HOSTS': [
            'host_1',
            'host_2',
        ],
        'MY_OTHER_SETTING': 'value_1',
    },
    'my_env_2': {
        'HOSTS': ['host_3'],
        'MY_OTHER_SETTING': 'value_2'
    }
}

fabfile.py

import sys
from fabric.api import env
from secrets import config


def _set_env(env_name):
    # can easily customize for various use cases
    selected_config = config.ENVS[env_name]
    for k, v in selected_config.items():
        setattr(env, k, v)


def _env_factory(env_dict):
    for k in env_dict:
        def func(k=k):
            _set_env(k)
        setattr(sys.modules[__name__], k, func)


_env_factory(config.ENVS)

def my_command():
    # do work
whp
fonte
0

Usar funções é atualmente considerado a maneira "adequada" e "correta" de fazer isso e é o que você "deve" fazer.

Dito isso, se você for como a maior parte do que você "gostaria" ou "desejo", é a capacidade de executar uma "manobra torcida" ou alternar sistemas de destino em tempo real.

Portanto, apenas para fins de entretenimento (!), O exemplo a seguir ilustra o que muitos podem considerar uma manobra arriscada, mas de alguma forma totalmente satisfatória, que é mais ou menos assim:

env.remote_hosts       = env.hosts = ['10.0.1.6']
env.remote_user        = env.user = 'bob'
env.remote_password    = env.password = 'password1'
env.remote_host_string = env.host_string

env.local_hosts        = ['127.0.0.1']
env.local_user         = 'mark'
env.local_password     = 'password2'

def perform_sumersault():
    env_local_host_string = env.host_string = env.local_user + '@' + env.local_hosts[0]
    env.password = env.local_password
    run("hostname -f")
    env.host_string = env.remote_host_string
    env.remote_password = env.password
    run("hostname -f")

Em seguida, executando:

fab perform_sumersault
user1180527
fonte