Jak ustawić hosty docelowe w pliku Fabric

107

Chcę używać Fabric do wdrażania kodu mojej aplikacji internetowej na serwerach deweloperskich, pomostowych i produkcyjnych. Mój plik fab:

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

Przykładowe dane wyjściowe:

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

Kiedy tworzę set_hosts()zadanie, jak pokazano w dokumentacji Fabric , env.hosts jest ustawiany poprawnie. Jednak nie jest to opłacalna opcja, podobnie jak dekorator. Przekazywanie hostów w wierszu poleceń ostatecznie doprowadziłoby do powstania jakiegoś skryptu powłoki, który wywołuje plik fabfile, wolałbym, aby jedno narzędzie wykonało zadanie poprawnie.

W dokumentacji Fabric jest napisane, że „env.hosts jest po prostu obiektem listy Pythona”. Z moich obserwacji wynika, że ​​to po prostu nieprawda.

Czy ktoś może wyjaśnić, co się tutaj dzieje? Jak ustawić hosta do wdrożenia?

ssc
źródło
Mam ten sam problem, czy znalazłeś jakieś rozwiązanie?
Martin M.
aby uruchomić to samo zadanie na wielu serwerach, użyj opcji „fab -H staging-server, production-server deploy” ... więcej w mojej odpowiedzi poniżej: stackoverflow.com/a/21458231/26510
Brad Parks,
Ta odpowiedź nie dotyczy tkaniny 2+. Jeśli ktoś bardziej zaznajomiony z konwencjami Stackoverflow mógłby edytować pytanie lub tytuł pytania, aby odnosić się do tkaniny 1, może to być pomocne.
Jonathan Berger

Odpowiedzi:

128

Robię to, deklarując rzeczywistą funkcję dla każdego środowiska. Na przykład:

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

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

def deploy():
    ...

Korzystając z powyższych funkcji, wpisałbym następujące polecenie, aby wdrożyć w moim środowisku testowym:

fab test deploy

... i następujące elementy do wdrożenia do produkcji:

fab prod deploy

Zaletą robienia tego w ten sposób jest to, że funkcji testi prodmożna używać przed każdą funkcją fab, a nie tylko wdrażaniem. Jest niesamowicie przydatny.

Zac
źródło
10
Ze względu na błąd w materiale ( code.fabfile.org/issues/show/138#change-1497 ) lepiej jest uwzględnić użytkownika w ciągu znaków hosta (np. [email protected]) zamiast ustawiać env.user.
Michaił Korobow
1
Miałem ten sam problem i to wydaje się najlepszym rozwiązaniem. Definiuję hostów, użytkownika i wiele innych ustawień w pliku YAML, który jest ładowany przez funkcje dev () i prod (). (Aby móc ponownie użyć tego samego skryptu Fabric do podobnych projektów.)
Christian Davén
@MikhailKorobov: Kiedy kliknąłem twój link, zobaczyłem „ Witamy w nginx! ”. Wszystkie żądania do code.fabfile.orgdomeny mają podobne odpowiedzi.
Tadeck
Tak, wygląda na to, że wszystkie błędy zostały przeniesione na github.
Michaił Korobow
2
Niestety wygląda na to, że to już nie działa - Fabric nie będzie uruchamiał zadań bez już zdefiniowanych env.hosts i nie będzie uruchamiał funkcji w fab A B Cstylu bez zdefiniowania ich jako zadań.
DNelson
77

Użyj 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')

Wybierz rolę za pomocą -R:

$ fab -R test deploy
[localhost] Executing task 'deploy'
...
thomie
źródło
7
Lub jeśli zadanie jest zawsze uruchamiane w tej samej roli, możesz użyć dekoratora @roles () w zadaniu.
Tom
2
Wygląda na to, że roledefs jest lepszym rozwiązaniem niż definiowanie ich w oddzielnych zadaniach.
Ehtesh Choudhury
Czy ktoś wie, jak mogę dołączyć hasło do podanej nazwy użytkownika w roledef? Kolejny wpis w słowniku 'password': 'some_password'wydaje się być ignorowany i prowadzi do zachęty w czasie wykonywania.
Dirk
@Dirk możesz użyć env.passwords, który jest słownikiem zawierającym użytkownik + host + port jako klucz i hasło jako wartość. Np. Env.passwords = {'user @ host: 22': 'password'}
Jonathan
49

Oto prostsza wersja odpowiedzi serverhorror :

from fabric.api import settings

def mystuff():
    with settings(host_string='192.0.2.78'):
        run("hostname -f")
tobych
źródło
2
Zgodnie z dokumentacją menedżer kontekstu ustawień służy do zastępowania envzmiennych, a nie do ich początkowego ustawiania. Myślę, że używanie roledefs , jak sugeruje thomie, jest bardziej odpowiednie do definiowania hostów, takich jak stage, dev i test.
Tony
21

Utknąłem na tym sam, ale w końcu to rozgryzłem. Po prostu nie możesz ustawić konfiguracji env.hosts z poziomu zadania. Każde zadanie jest wykonywane N razy, raz dla każdego określonego hosta, więc ustawienie zasadniczo wykracza poza zakres zadania.

Patrząc na swój kod powyżej, możesz po prostu zrobić to:

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

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

def deploy():
    # do stuff...

Co wydaje się, że zrobi to, co zamierzasz.

Możesz też napisać niestandardowy kod w zakresie globalnym, który ręcznie analizuje argumenty i ustawia env.hosts przed zdefiniowaniem funkcji zadania. Z kilku powodów tak właśnie skonfigurowałem mój.

Złoty chłopak
źródło
Znalazłem sposób from fabric.api import env:; env.host_string = "dev"
Roman
18

Od fab 1.5 jest to udokumentowany sposób na dynamiczne ustawianie hostów.

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

Cytat z poniższego dokumentu.

Korzystanie z funkcji wykonaj z dynamicznie ustawianymi listami hostów

Typowym przypadkiem użycia średnio-zaawansowanego Fabric dla Fabric jest parametryzacja wyszukiwania listy hostów docelowych w czasie wykonywania (gdy użycie ról nie wystarcza). execute może uczynić to niezwykle prostym, na przykład:

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
źródło
3
+1. Wiele naprawdę dobrych odpowiedzi na dole strony.
Matt Montag,
10

W przeciwieństwie do niektórych innych odpowiedzi, to jest możliwe, aby zmodyfikować envzmienne środowiskowe w ramach zadania. Jednak envbędzie to używane tylko do kolejnych zadań wykonywanych przy użyciu fabric.tasks.executefunkcji.

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

Bez zawijania pod-zadań execute(...), zostaną użyte envustawienia na poziomie modułu lub cokolwiek zostanie przekazane z fabCLI.

pztrick
źródło
To najlepsza odpowiedź, jeśli chcesz dynamicznie ustawiać env.hosts.
JahMyst
9

Musisz host_stringpodać przykład:

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.
źródło
Słodkie. W innej odpowiedzi zamieściłem prostszą wersję kodu.
tobych
9

Aby wyjaśnić, dlaczego to w ogóle problem. Polecenie fab wykorzystuje strukturę biblioteki do wykonywania zadań na listach hostów. Jeśli spróbujesz zmienić listę hostów w zadaniu, zasadniczo próbujesz zmienić listę podczas iteracji po niej. Lub w przypadku, gdy nie masz zdefiniowanych żadnych hostów, zapętlaj pustą listę, w której kod, w którym ustawiono pętlę listy, nigdy nie jest wykonywany.

Użycie env.host_string jest obejściem tego zachowania tylko dlatego, że określa bezpośrednio w funkcjach, z którymi hostami mają się łączyć. Powoduje to pewne problemy polegające na tym, że przerobisz pętlę wykonywania, jeśli chcesz mieć kilka hostów do wykonania.

Najprostszym sposobem, w jaki ludzie udostępniają możliwość ustawiania hostów w czasie wykonywania, jest utrzymywanie zapełniania środowiska środowiska jako odrębnego zadania, które konfiguruje wszystkie ciągi znaków hosta, użytkowników itp. Następnie uruchamiają zadanie wdrażania. To wygląda tak:

fab production deploy

lub

fab staging deploy

Tam, gdzie inscenizacja i produkcja są podobne do zadań, które powierzyłeś, ale same nie nazywają następnego zadania. Powodem, dla którego musi tak działać, jest to, że zadanie musi zakończyć się i wyjść z pętli (hostów, w przypadku env Brak, ale w tym momencie jest to pętla jednego), a następnie pętla hosty (teraz zdefiniowane przez poprzednie zadanie) na nowo.

Morgan
źródło
3

Musisz zmodyfikować env.hosts na poziomie modułu, a nie w ramach funkcji zadania. Popełniłem ten sam błąd.

from fabric.api import *

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

env.hosts = _get_hosts()

def your_task():
    ... your task ...
mlbright
źródło
3

To jest bardzo proste. Wystarczy zainicjować zmienną env.host_string i wszystkie poniższe polecenia zostaną wykonane na tym hoście.

from fabric.api import env, run

env.host_string = '[email protected]'

def foo:
    run("hostname -f")
Vladimir Osintsev
źródło
3

Jestem zupełnie nowy w strukturze, ale aby uzyskać Fabric do uruchamiania tych samych poleceń na wielu hostach (np. Aby wdrożyć na wielu serwerach, w jednym poleceniu), możesz uruchomić:

fab -H staging-server,production-server deploy 

gdzie staging-serwer i produkcyjno-serwer są 2 serwery chcesz uruchomić akcję wdrożyć przeciwko. Oto prosty plik fabfile.py, który wyświetli nazwę systemu operacyjnego. Zauważ, że plik fabfile.py powinien znajdować się w tym samym katalogu, w którym uruchamiasz polecenie fab.

from fabric.api import *

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

Działa to przynajmniej z tkaniną 1.8.1.

Brad Parks
źródło
3

Tak więc, aby ustawić hosty i uruchomić polecenia na wszystkich hostach, musisz zacząć od:

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

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

Po ich zdefiniowaniu uruchom polecenie w wierszu poleceń:

fab PROD deploy:1.5

Co spowoduje uruchomienie zadania wdrażania na wszystkich serwerach wymienionych w funkcji PROD, ponieważ ustawia env.hosts przed uruchomieniem zadania.

Athros
źródło
Załóżmy, że wdrożenie na pierwszym hoście zadziałało, ale jedno na drugim nie powiodło się, jak mam to zrobić ponownie tylko na drugim?
nr
2

Możesz przypisać do env.hoststringprzed wykonaniem podzadania. Przypisz do tej zmiennej globalnej w pętli, jeśli chcesz iterować na wielu hostach.

Niestety dla ciebie i dla mnie tkanina nie jest przeznaczona do tego przypadku. Sprawdź mainfunkcję na http://github.com/bitprophet/fabric/blob/master/fabric/main.py, aby zobaczyć, jak to działa.

Andrew B.
źródło
2

Oto kolejny wzorzec „summersault”, który umożliwia rozszerzenie fab my_env_1 my_command użycie:

W przypadku tego wzorca wystarczy, że raz zdefiniujemy środowiska za pomocą słownika. env_factorytworzy funkcje na podstawie nazw kluczy ENVS. Umieściłem ENVSwe własnym katalogu i plikusecrets.config.py aby oddzielić konfigurację od kodu tkaniny.

Wadą jest to, że jak napisano, dodanie @taskdekoratora spowoduje przerwanie .

Uwagi: Używamy def func(k=k):zamiast def func():w fabryce z powodu późnego wiązania . Otrzymujemy działający moduł z tym rozwiązaniem i poprawiamy go, aby zdefiniować funkcję.

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
źródło
0

Używanie ról jest obecnie uważane za „właściwy” i „prawidłowy” sposób robienia tego i jest tym, co „powinieneś” robić.

To powiedziawszy, jeśli jesteś podobny do większości tego, co „chciałbyś” lub „pragniesz”, to umiejętność wykonywania „skręconej potyczki” lub przełączania systemów docelowych w locie.

Tak więc, wyłącznie dla celów rozrywkowych (!), Poniższy przykład ilustruje, co wielu może uznać za ryzykowny, a jednocześnie w pełni satysfakcjonujący manewr, który wygląda mniej więcej tak:

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

Następnie bieganie:

fab perform_sumersault
user1180527
źródło