Comment définir les hôtes cibles dans le fichier Fabric

107

Je souhaite utiliser Fabric pour déployer le code de mon application Web sur des serveurs de développement, de préparation et de production. Mon 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

Exemple de sortie:

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

Lorsque je crée une set_hosts()tâche comme indiqué dans la documentation Fabric , env.hosts est correctement défini. Cependant, ce n'est pas une option viable, pas plus qu'un décorateur. Passer des hôtes sur la ligne de commande aboutirait finalement à une sorte de script shell qui appelle le fabfile, je préférerais qu'un seul outil fasse le travail correctement.

Il dit dans la documentation Fabric que «env.hosts est simplement un objet de liste Python». D'après mes observations, ce n'est tout simplement pas vrai.

Quelqu'un peut-il expliquer ce qui se passe ici? Comment puis-je configurer l'hôte sur lequel effectuer le déploiement?

ssc
la source
J'ai le même problème, avez-vous trouvé une solution à cela?
Martin M.
pour exécuter la même tâche sur plusieurs serveurs, utilisez "fab -H staging-server, production-server deploy" ... plus dans ma réponse ci-dessous: stackoverflow.com/a/21458231/26510
Brad Parks
Cette réponse ne s'applique pas au tissu 2+. Si quelqu'un plus familier avec les conventions de Stackoverflow pouvait modifier la question ou le titre de la question pour faire référence au fabric 1, cela pourrait être utile.
Jonathan Berger

Réponses:

128

Je fais cela en déclarant une fonction réelle pour chaque environnement. Par exemple:

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

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

def deploy():
    ...

En utilisant les fonctions ci-dessus, je taperais ce qui suit pour le déployer dans mon environnement de test:

fab test deploy

... et les éléments suivants à déployer en production:

fab prod deploy

La bonne chose à faire de cette façon est que les fonctions testet prodpeuvent être utilisées avant toute fonction fab, pas seulement déployer. C'est incroyablement utile.

Zac
la source
10
En raison d'un bogue dans fabric ( code.fabfile.org/issues/show/138#change-1497 ), il est préférable d'inclure l'utilisateur dans la chaîne hôte (comme [email protected]) au lieu de définir env.user.
Mikhail Korobov
1
J'ai eu le même problème, et cela semble être la meilleure solution. Je définis les hôtes, l'utilisateur et de nombreux autres paramètres dans un fichier YAML chargé par les fonctions dev () et prod (). (Pour que je puisse réutiliser le même script Fabric pour des projets similaires.)
Christian Davén
@MikhailKorobov: Quand j'ai suivi votre lien, j'ai vu " Welcome to nginx! ". Toutes les demandes de code.fabfile.orgdomaine ont des réponses comme ça.
Tadeck
Ouais, il semble que tous les bogues ont été migrés vers github.
Mikhail Korobov
2
Malheureusement, il semble que cela ne fonctionne plus - fabric ne lancera pas de tâches sans env.hosts déjà définis et n'exécutera pas de fonctions dans le fab A B Cstyle sans qu'elles soient définies comme des tâches.
DNelson
77

Utiliser des 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')

Choisissez le rôle avec -R:

$ fab -R test deploy
[localhost] Executing task 'deploy'
...
thomie
la source
7
Ou si la tâche est toujours exécutée sur le même rôle, vous pouvez utiliser le décorateur @roles () sur la tâche.
Tom
2
On dirait que les roledefs sont une meilleure solution que de les définir dans des tâches distinctes.
Ehtesh Choudhury
Quelqu'un sait-il comment je peux inclure un mot de passe pour le nom d'utilisateur fourni dans un roledef? Une autre entrée de dictionnaire 'password': 'some_password'semble être ignorée et conduit à une invite au moment de l'exécution.
Dirk le
@Dirk vous pouvez utiliser env.passwords qui est un dictionnaire contenant user + host + port comme clé et mot de passe comme valeur. Par exemple, env.passwords = {'user @ host: 22': 'password'}
Jonathan
49

Voici une version plus simple de la réponse de serverhorror :

from fabric.api import settings

def mystuff():
    with settings(host_string='192.0.2.78'):
        run("hostname -f")
tobych
la source
2
Selon la documentation , le gestionnaire de contexte des paramètres sert à remplacer les envvariables, pas à les définir initialement. Je pense que l'utilisation de roledefs , comme l'a suggéré Thomie, est plus appropriée pour définir des hôtes tels que stage, dev et test.
Tony
21

J'étais coincé moi-même, mais je l'ai finalement compris. Vous ne pouvez pas définir la configuration de env.hosts de l' intérieur d' une tâche. Chaque tâche est exécutée N fois, une fois pour chaque hôte spécifié, de sorte que le paramètre est fondamentalement en dehors de la portée de la tâche.

En regardant votre code ci-dessus, vous pouvez simplement faire ceci:

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

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

def deploy():
    # do stuff...

Ce qui semble faire ce que vous avez l'intention.

Ou vous pouvez écrire du code personnalisé dans la portée globale qui analyse les arguments manuellement et définit env.hosts avant que votre fonction de tâche ne soit définie. Pour plusieurs raisons, c'est en fait ainsi que j'ai mis en place le mien.

GoldenBoy
la source
Trouvé un moyen from fabric.api import env:; env.host_string = "dev"
Roman
18

Depuis fab 1.5, c'est un moyen documenté de définir dynamiquement des hôtes.

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

Citation du doc ​​ci-dessous.

Utilisation de l'exécution avec des listes d'hôtes définies dynamiquement

Un cas d'utilisation intermédiaire à avancé commun pour Fabric consiste à paramétrer la recherche de sa liste d'hôtes cible au moment de l'exécution (lorsque l'utilisation de rôles ne suffit pas). execute peut rendre cela extrêmement simple, comme ceci:

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
la source
3
+1. Beaucoup de très bonnes réponses vers le bas de la page ici.
Matt Montag
10

Contrairement à certaines autres réponses, il est possible de modifier les envvariables d'environnement au sein d'une tâche. Cependant, cela envne sera utilisé que pour les tâches suivantes exécutées à l'aide de la fabric.tasks.executefonction.

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

Sans encapsuler les sous-tâches dans execute(...), vos envparamètres au niveau du module ou tout ce qui est transmis par l' fabinterface de ligne de commande seront utilisés.

pztrick
la source
C'est la meilleure réponse si vous souhaitez définir dynamiquement des env.hosts.
JahMyst
9

Vous devez donner host_stringun exemple:

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.
la source
Doux. J'ai publié une version plus simple du code dans une autre réponse ici.
tobych
9

Pour expliquer pourquoi c'est même un problème. La commande fab utilise la bibliothèque de fabric pour exécuter les tâches sur les listes d'hôtes. Si vous essayez de modifier la liste d'hôtes dans une tâche, vous essayez essentiellement de modifier une liste tout en l'itérant. Ou dans le cas où vous n'avez pas d'hôte défini, bouclez sur une liste vide où le code sur lequel vous définissez la liste pour boucler n'est jamais exécuté.

L'utilisation d'env.host_string est une solution de contournement pour ce comportement uniquement en ce sens qu'elle spécifie directement aux fonctions les hôtes avec lesquels se connecter. Cela pose certains problèmes dans la mesure où vous allez refaire la boucle d'exécution si vous souhaitez avoir un certain nombre d'hôtes sur lesquels exécuter.

Le moyen le plus simple pour les utilisateurs de définir les hôtes au moment de l'exécution est de conserver l'environnement en tant que tâche distincte, qui configure toutes les chaînes d'hôte, les utilisateurs, etc. Ensuite, ils exécutent la tâche de déploiement. Cela ressemble à ceci:

fab production deploy

ou

fab staging deploy

Où la mise en scène et la production sont comme les tâches que vous avez données, mais elles n'appellent pas la tâche suivante elles-mêmes. La raison pour laquelle cela doit fonctionner comme ça, c'est que la tâche doit se terminer, et sortir de la boucle (des hôtes, dans le cas env None, mais c'est une boucle d'un à ce point), puis avoir la boucle sur les hôtes (maintenant définis par la tâche précédente) à nouveau.

Morgan
la source
3

Vous devez modifier env.hosts au niveau du module, pas dans une fonction de tâche. J'ai fait la même erreur.

from fabric.api import *

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

env.hosts = _get_hosts()

def your_task():
    ... your task ...
mlbright
la source
3

C'est très simple. Initialisez simplement la variable env.host_string et toutes les commandes suivantes seront exécutées sur cet hôte.

from fabric.api import env, run

env.host_string = '[email protected]'

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

Je suis totalement nouveau dans Fabric, mais pour que Fabric exécute les mêmes commandes sur plusieurs hôtes (par exemple pour déployer sur plusieurs serveurs, en une seule commande), vous pouvez exécuter:

fab -H staging-server,production-server deploy 

staging-server et production-server sont 2 serveurs sur lesquels vous souhaitez exécuter l'action de déploiement. Voici un fabfile.py simple qui affichera le nom du système d'exploitation. Notez que fabfile.py doit être dans le même répertoire que celui où vous exécutez la commande fab.

from fabric.api import *

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

Cela fonctionne au moins avec le tissu 1.8.1.

Brad Parks
la source
3

Donc, pour définir les hôtes et faire exécuter les commandes sur tous les hôtes, vous devez commencer par:

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

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

Une fois ceux-ci définis, exécutez la commande sur la ligne de commande:

fab PROD deploy:1.5

Qu'est-ce qui exécutera la tâche de déploiement sur tous les serveurs répertoriés dans la fonction PROD, car elle définit les hôtes env. Avant d'exécuter la tâche.

athros
la source
Supposons que le déploiement sur le premier hôte a fonctionné mais que celui sur le second a échoué, comment puis-je le refaire uniquement sur le second?
nos
2

Vous pouvez affecter à env.hoststringavant d'exécuter une sous-tâche. Affectez à cette variable globale dans une boucle si vous souhaitez effectuer une itération sur plusieurs hôtes.

Malheureusement pour vous et moi, le tissu n'est pas conçu pour ce cas d'utilisation. Découvrez la mainfonction sur http://github.com/bitprophet/fabric/blob/master/fabric/main.py pour voir comment cela fonctionne.

Andrew B.
la source
2

Voici un autre modèle "summersault" qui permet l' fab my_env_1 my_commandutilisation:

Avec ce modèle, nous n'avons à définir les environnements qu'une seule fois à l'aide d'un dictionnaire. env_factorycrée des fonctions basées sur les noms clés de ENVS. J'ai mis ENVSdans son propre répertoire et fichier secrets.config.pypour séparer la configuration du code du fabric.

L'inconvénient est que, comme écrit, l'ajout du @taskdécorateur le cassera .

Remarques: Nous utilisons à la def func(k=k):place de def func():l'usine en raison d' une liaison tardive . Nous obtenons le module en cours d'exécution avec cette solution et le corrigeons pour définir la fonction.

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
la source
0

L'utilisation des rôles est actuellement considérée comme la manière «appropriée» et «correcte» de le faire et c'est ce que vous «devriez» faire.

Cela dit, si vous êtes comme la plupart de ce que vous «voudriez» ou «désirez», c'est la possibilité d'effectuer un «système tordu» ou de changer de système cible à la volée.

Donc, à des fins de divertissement uniquement (!), L'exemple suivant illustre ce que beaucoup pourraient considérer comme une manœuvre risquée, mais en quelque sorte totalement satisfaisante, qui ressemble à ceci:

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

Puis en cours d'exécution:

fab perform_sumersault
user1180527
la source