Cómo configurar hosts de destino en el archivo Fabric

107

Quiero usar Fabric para implementar el código de mi aplicación web en servidores de desarrollo, preparación y producción. Mi 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

Salida de muestra:

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

Cuando creo una set_hosts()tarea como se muestra en los documentos de Fabric , env.hosts está configurado correctamente. Sin embargo, esta no es una opción viable, ni tampoco un decorador. Pasar hosts en la línea de comando daría como resultado en última instancia algún tipo de script de shell que llame al fabfile, preferiría que una sola herramienta hiciera el trabajo correctamente.

Dice en los documentos de Fabric que 'env.hosts es simplemente un objeto de lista de Python'. Según mis observaciones, esto simplemente no es cierto.

¿Alguien puede explicar qué está pasando aquí? ¿Cómo puedo configurar el host para la implementación?

ssc
fuente
Tengo el mismo problema, ¿has encontrado alguna solución a esto?
Martin M.
para ejecutar la misma tarea en varios servidores, use "fab -H staging-server, production-server deploy" ... más en mi respuesta a continuación: stackoverflow.com/a/21458231/26510
Brad Parks
Esta respuesta no se aplica a la tela 2+. Si alguien más familiarizado con las convenciones de Stackoverflow pudiera editar la pregunta o el título de la pregunta para hacer referencia a la estructura 1, podría ser útil.
Jonathan Berger

Respuestas:

128

Hago esto declarando una función real para cada entorno. Por ejemplo:

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

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

def deploy():
    ...

Usando las funciones anteriores, escribiría lo siguiente para implementarlo en mi entorno de prueba:

fab test deploy

... y lo siguiente para implementar en producción:

fab prod deploy

Lo bueno de hacerlo de esta manera es que las funciones testy prodse pueden usar antes que cualquier función fab, no solo implementar. Es increíblemente útil.

Zac
fuente
10
Debido a un error en la estructura ( code.fabfile.org/issues/show/138#change-1497 ) es mejor incluir al usuario en la cadena de host (como [email protected]) en lugar de configurar env.user.
Mikhail Korobov
1
Tuve el mismo problema y esta parece ser la mejor solución. Defino los hosts, el usuario y muchas otras configuraciones en un archivo YAML que cargan las funciones dev () y prod (). (Para poder reutilizar el mismo guión de Fabric para proyectos similares.)
Christian Davén
@MikhailKorobov: Cuando seguí tu enlace, vi "¡ Bienvenido a nginx! ". Todas las solicitudes de code.fabfile.orgdominio tienen respuestas así.
Tadeck
Sí, parece que todos los errores se migraron a github.
Mikhail Korobov
2
Desafortunadamente, parece que esto ya no funciona: fabric no ejecutará tareas sin env.hosts ya definidos y no ejecutará funciones en el fab A B Cestilo sin que estén definidas como tareas.
DNelson
77

Utilice 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')

Elija el rol con -R:

$ fab -R test deploy
[localhost] Executing task 'deploy'
...
thomie
fuente
7
O si la tarea siempre se ejecuta en el mismo rol, puede usar el decorador @roles () en la tarea.
Tom
2
Parece que roledefs es una mejor solución que definirlos en tareas separadas.
Ehtesh Choudhury
¿Alguien sabe cómo puedo incluir una contraseña para el nombre de usuario proporcionado en un roledef? Una entrada adicional del diccionario 'password': 'some_password'parece ignorarse y conduce a un mensaje en tiempo de ejecución.
Dirk
@Dirk puede usar env.passwords, que es un diccionario que contiene usuario + host + puerto como clave y contraseña como valor. Por ejemplo, env.passwords = {'usuario @ host: 22': 'contraseña'}
Jonathan
49

Aquí hay una versión más simple de la respuesta de serverhorror :

from fabric.api import settings

def mystuff():
    with settings(host_string='192.0.2.78'):
        run("hostname -f")
tobych
fuente
2
Según los documentos , el administrador de contexto de configuración es para anular envvariables, no para configurarlas inicialmente. Creo que el uso de roledefs , como sugirió thomie, es más apropiado para definir hosts como stage, dev y test.
Tony
21

Estaba atrapado en esto yo mismo, pero finalmente lo descubrí. Simplemente no puede establecer la configuración de env.hosts desde dentro de una tarea. Cada tarea se ejecuta N veces, una para cada Host especificado, por lo que la configuración está fundamentalmente fuera del alcance de la tarea.

Mirando su código anterior, simplemente podría hacer esto:

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

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

def deploy():
    # do stuff...

Lo que parece que haría lo que pretendes.

O puede escribir código personalizado en el ámbito global que analice los argumentos manualmente y establezca env.hosts antes de que se defina la función de la tarea. Por algunas razones, así es como configuré el mío.

Chico de oro
fuente
Encontré una manera from fabric.api import env:; env.host_string = "dev"
Roman
18

Desde fab 1.5, esta es una forma documentada de configurar hosts dinámicamente.

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

Cita del documento a continuación.

Uso de ejecutar con listas de hosts configuradas dinámicamente

Un caso de uso común de intermedio a avanzado para Fabric es parametrizar la búsqueda de la lista de hosts de destino en tiempo de ejecución (cuando el uso de roles no es suficiente). ejecutar puede hacer esto extremadamente simple, así:

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
fuente
3
+1. Aquí hay muchas respuestas realmente buenas en la parte inferior de la página.
Matt Montag
10

A diferencia de otras respuestas, es posible modificar las envvariables de entorno dentro de una tarea. Sin embargo, esto envsolo se usará para tareas posteriores ejecutadas con la fabric.tasks.executefunción.

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

Sin incluir las subtareas , se utilizará la configuración de execute(...)nivel de módulo envo lo que se pase de la fabCLI.

pztrick
fuente
Esta es la mejor respuesta si desea configurar env.hosts dinámicamente.
JahMyst
9

Necesitas dar host_stringun ejemplo sería:

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, ))
Martín M.
fuente
Dulce. Publiqué una versión más simple del código en otra respuesta aquí.
tobych
9

Para explicar por qué es incluso un problema. El comando fab está aprovechando la estructura de la biblioteca para ejecutar las tareas en las listas de hosts. Si intenta cambiar la lista de hosts dentro de una tarea, esencialmente está intentando cambiar una lista mientras la itera. O, en el caso de que no tenga hosts definidos, recorra una lista vacía donde el código donde configuró la lista para que se repita nunca se ejecuta.

El uso de env.host_string es una solución para este comportamiento solo en el sentido de que especifica directamente a las funciones con qué hosts conectarse. Esto causa algunos problemas en el sentido de que rehacerá el ciclo de ejecución si desea tener varios hosts para ejecutar.

La forma más sencilla en que la gente crea la capacidad de configurar hosts en tiempo de ejecución es mantener la población de env como una tarea distinta, que configura todas las cadenas de host, usuarios, etc. Luego, ejecutan la tarea de implementación. Se parece a esto:

fab production deploy

o

fab staging deploy

Donde la puesta en escena y la producción son como las tareas que le ha encomendado, pero no llaman a la siguiente tarea por sí mismas. La razón por la que tiene que funcionar así es que la tarea tiene que terminar y salir del bucle (de hosts, en el caso de env None, pero es un bucle de uno en ese punto), y luego tener el bucle terminado los hosts (ahora definidos por la tarea anterior) de nuevo.

Morgan
fuente
3

Necesita modificar env.hosts a nivel de módulo, no dentro de una función de tarea. Yo cometí el mismo error.

from fabric.api import *

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

env.hosts = _get_hosts()

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

Es muy simple. Simplemente inicialice la variable env.host_string y todos los siguientes comandos se ejecutarán en este host.

from fabric.api import env, run

env.host_string = '[email protected]'

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

Soy totalmente nuevo en Fabric, pero para que Fabric ejecute los mismos comandos en múltiples hosts (por ejemplo, para implementar en múltiples servidores, en un comando), puede ejecutar:

fab -H staging-server,production-server deploy 

donde el servidor intermedio y el servidor de producción son 2 servidores contra los que desea ejecutar la acción de implementación. Aquí hay un simple fabfile.py que mostrará el nombre del sistema operativo. Tenga en cuenta que fabfile.py debe estar en el mismo directorio donde ejecuta el comando fab.

from fabric.api import *

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

Esto funciona con la tela 1.8.1 al menos.

Brad Parks
fuente
3

Entonces, para configurar los hosts y hacer que los comandos se ejecuten en todos los hosts, debe comenzar con:

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

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

Una vez que estén definidos, ejecute el comando en la línea de comando:

fab PROD deploy:1.5

Qué ejecutará la tarea de implementación en todos los servidores enumerados en la función PROD, ya que establece env.hosts antes de ejecutar la tarea.

athros
fuente
Supongamos que la implementación en el primer host funcionó pero la del segundo falló, ¿cómo lo hago nuevamente solo en el segundo?
nos
2

Puede asignar env.hoststringantes de ejecutar una subtarea. Asigne a esta variable global en un bucle si desea iterar sobre múltiples hosts.

Desafortunadamente para usted y para mí, la tela no está diseñada para este caso de uso. Consulte la mainfunción en http://github.com/bitprophet/fabric/blob/master/fabric/main.py para ver cómo funciona.

Andrew B.
fuente
2

Aquí hay otro patrón de "salto de verano" que permite fab my_env_1 my_command uso:

Con este patrón, solo tenemos que definir entornos una vez utilizando un diccionario. env_factorycrea funciones basadas en los nombres clave de ENVS. Puse ENVSsu propio directorio y archivo secrets.config.pypara separar la configuración del código de la tela.

El inconveniente es que, como está escrito, agregar el @taskdecorador lo romperá .

Notas: Usamos en def func(k=k):lugar de def func():en la fábrica debido a la encuadernación tardía . Obtenemos el módulo en ejecución con esta solución y lo parcheamos para definir la función.

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
fuente
0

Actualmente, el uso de roles se considera la forma "adecuada" y "correcta" de hacer esto y es lo que "debería" hacerlo.

Dicho esto, si usted es como la mayor parte de lo que "le gustaría" o "desea" es la capacidad de realizar un "sistema retorcido" o cambiar los sistemas de destino sobre la marcha.

Entonces, solo para fines de entretenimiento (!) El siguiente ejemplo ilustra lo que muchos podrían considerar una maniobra arriesgada y, sin embargo, completamente satisfactoria, que es algo como esto:

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")

Luego corriendo:

fab perform_sumersault
usuario1180527
fuente