понедельник, 28 января 2013 г.

Fabric: Обзор и руководство (Перевод)

Добро пожаловать в Fabric!

Этот документ - быстрый тур по возможностям Fabric и короткое руководство по его использованию. Дополнительная документация может быть найдена тут.

Что такое Fabric?

Как написано в README:
Fabric - это библиотека для Python (2.5 или выше) и инструмент командной строки для использования SSH при развёртывании приложений или выполнении административных задач.
Более конкретно: 

  • Инструмент, который позволяет Вам выполнить любую функцию Python при помощи командной строки
  • Библиотека подпрограмм (построенная на более низкоуровневой библиотеке), переназначенная для выполнения команд оболочки через SSH легко и по-питонски.
Обычно, большинство пользователей используют обе эти возможности, применяя Fabric для записи и выполнения функций Python или заданий, чтобы автоматизировать работу с удалённым сервером. Давайте на это посмотрим.

Hello, fab

Это не было бы хорошим руководством без стандартного примера:
def hello():
    print("Hello world!")
Если разместить эту функцию в модуле с именем fabfile.py в вашей рабочей директории, то эту функцию hello можно выполнить при помощи инструмента fab (устанавливаемого как часть Fabric) и будет делать именно то, что Вы ожидаете:
$ fab hello
Hello world!

Done.
Вот и всё. Таким образом Вы можете использовать Fabric как (очень) простой способ сборки даже без импорта какого либо из его API.
Примечание: Инструмент fab просто просто импортирует ваш fabfile и выполняет функцию или функции, которые Вы в нём определили. Тут нет никакой магии - всё, что Вы можете сделать в обычном скрипте Python, можно сделать и в fabfile.

Аргументы задачи

Очень часто полезно передать параметры в процессе выполнения в вашу задачу, как Вы это можете сделать и в обычном скрипте Python. Fabric поддерживает эту возможность при помощи такой нотации: <task name>:<arg>,<kwarg>=<value>,.... Давайте расширим наш первый пример:
def hello(name="world"):
    print("Hello %s!" % name)
По умолчанию, если Вы используете команду fab hello - Вы получите тот же результат, что и в раньше; но теперь Вы можете передать и имя, кого приветствовать:
$ fab hello:name=Jeff
Hello Jeff!

Done.
Те, кто уже программируют на Python, могут предположить, что то же самое можно сделать и немного по другому:
$ fab hello:Jeff
Hello Jeff!

Done.
На данный момент, аргумент, который Вы передаёте, в любом случае будет передан как строка, так что Вам могут потребоваться дополнительные манипуляции с полученным значением. В более поздних версиях может быть добавлена система типов, чтобы облегчить этот процесс.

Локальные команды

В примере выше fab всего лишь экономит Вам несколько строк, начинающихся после `if __name__=="__main__"`. Обычно же используется API Fabric, который содержит функции (или операции) для выполнения команд оболочки, передачи файлов и т.д.
Давайте построим гипотетическое Web приложение fabfile. Этот сценарий делает следующее: Web приложение управляется через Git на удалённом хосте vcshost. На localhost у нас есть локальный клон web-приложения. Когда мы размещаем обновления на vcshost, мы хотим иметь возможность немедленно установить эти обновления на удалённых хост my_server в автоматическом режиме. Мы будем делать это, автоматизируя локальные и удалённые команды Git.
Fabfile обычно лучше всего работают из корня проекта:
    .
    |-- __init__.py
    |-- app.wsgi
    |-- fabfile.py <-- our fabfile!
    |-- manage.py
    `-- my_app
        |-- __init__.py
        |-- models.py
        |-- templates
        |   `-- index.html
        |-- tests.py
        |-- urls.py
        `-- views.py
Примечание: мы будем использовать приложение Django, но лишь в качестве примера - Fabric не привязан к какому-то внешнему коду, используя лишь SSH библиотеку.
Для начала, возможно, мы захотим запустить наши тесты и отправить обновления на наш VCS, так что мы будем готовы к развёртыванию:
    from fabric.api import local

    def prepare_deploy():
        local("./manage.py test my_app")
        local("git add -p && git commit")
        local("git push")
Результат этого может быть таким:
    $ fab prepare_deploy
    [localhost] run: ./manage.py test my_app
    Creating test database...
    Creating tables
    Creating indexes
    ..........................................
    ----------------------------------------------------------------------
    Ran 42 tests in 9.138s

    OK
    Destroying test database...

    [localhost] run: git add -p && git commit

    <interactive Git add / git commit edit message session>

    [localhost] run: git push

    <git push session, possibly merging conflicts interactively>

    Done.
Сам код вполне понятен: мы импортируем функцию API Fabric, local, и используем её для запуска и взаимодействия с локальной оболочкой. Все остальные API Fabric аналогичны - это лишь Python.

Сделайте это по своему

Так как Fabric - "всего лишь Python", Вы можете организовать свой fabfile так, как Вам это удобно. Например, часто полезно выделить некоторые вещи в подзадачи:
    from fabric.api import local

    def test():
        local("./manage.py test my_app")

    def commit():
        local("git add -p && git commit")

    def push():
        local("git push")

    def prepare_deploy():
        test()
        commit()
        push()
Задача prepare_deploy может быть вызвана как в примерах выше, но теперь Вы можете использовать и более детализированные подзадачи, если захотите.

Ошибки

Всё может работать хорошо, но что будет, если наш тест завершится с ошибкой? Скорее всего, мы захотим остановить наш процесс и сперва исправить ошибку.
Fabric проверяет возвращаемое значение программы, которую он вызывает, и прерывает своё выполнение если вызванная программа завершилась как-то не так. Давайте посмотрим, что будет, если один из наших тестов завершится с ошибкой:
    $ fab prepare_deploy
    [localhost] run: ./manage.py test my_app
    Creating test database...
    Creating tables
    Creating indexes
    .............E............................
    ======================================================================
    ERROR: testSomething (my_project.my_app.tests.MainTests)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
    [...]

    ----------------------------------------------------------------------
    Ran 42 tests in 9.138s

    FAILED (errors=1)
    Destroying test database...

    Fatal error: local() encountered an error (return code 2) while executing './manage.py test my_app'

    Aborting.
Замечательно! Нам даже ничего не нужно делать: Fabric обнаружил ошибку и зам завершил свою работу, не дойдя до ``commit``.

Обработка ошибок

Но что если мы хотим быть немного более гибкими и дать пользователю выбор? Настройка (или переменная окружения, обычно сокращается до env var) под названием warn_only позволяет не прерывать выполнение, а лишь вывести предупреждение, что даёт возможность организовать гибкую обработку ошибок.
Давайте воспользуемся этой настройкой для нашей функции test и затем посмотрим на результат вызова local:

    from __future__ import with_statement
    from fabric.api import local, settings, abort
    from fabric.contrib.console import confirm

    def test():
        with settings(warn_only=True):
            result = local('./manage.py test my_app', capture=True)
        if result.failed and not confirm("Tests failed. Continue anyway?"):
            abort("Aborting at user request.")

    [...]

Кроме того, о чём мы уже сказали, мы воспользовались тут ещё несколькими новыми вещами:

  • Импорт __future__ необходим для использования with в Python 2.5
  • Подмодуль contrib.console содержит функцию confirm, которая используется для вывода в консоль вопроса да /нет
  • Менеджер контекста settings используется для применения конкретных настроек к отдельному блоку кода
  • Операция запуска команд наподобие local может вернуть объекты, содержащие информацию о результате выполнения запущенной команды (например .failed или .return_code)
  • Функция abort используется для ручного прерывания программы
И, несмотря на некоторое усложнение, код всё ещё прост и достаточно гибок.

Создаём подключения

Давайте улучшим наш fabfile, добавив туда задачу deploy, которая будет запускаться на удалённом сервере и проверять, что находящийся на нём код актуален:
    def deploy():
        code_dir = '/srv/django/myproject'
        with cd(code_dir):
            run("git pull")
            run("touch app.wsgi")
Опять же, тут мы используем некоторые новые возможности:
  • Fabric это лишь Python, так что мы можем свободно использовать обычный код Python, переменные и строки
  • cd - простой способ предварить команду вызовом cd /в/какую-либо/директорию (то есть изменить каталог перед вызовом другой команды). Это аналогично команде lcd, которая делает то же самое на локальной машине
  • run аналогичен local, но запускает команду удалённо, а не локально
Кроме того, мы должны быть уверены, что мы импортировали новые функции в начале файла:
    from __future__ import with_statement
    from fabric.api import local, settings, abort, run, cd
    from fabric.contrib.console import confirm
Теперь со всеми этими изменениями пора приниматься за новый сервер:
    $ fab deploy
    No hosts found. Please specify (single) host string for connection: my_server
    [my_server] run: git pull
    [my_server] out: Already up-to-date.
    [my_server] out:
    [my_server] run: touch app.wsgi

    Done.
Но мы ещё не создали ни одного подключения, так что Fabric не знает, на каком удалённом хосте должны выполняться эти команды. В таком случае Fabric спросит это у нас в процессе выполнения задачи. Определения подключения задаётся в форме SSH подобной строки подключения, т.е. user@host:port и по умолчанию будет использовать локальное имя пользователя, так что в нашем примере достаточно указать только имя сервера - my_server.

Взаимодействие с удалённым сервером

git pull работает хорошо если Вы уже получили исходники, но что если это первое получение кода? Неплохо было бы обработать и эту ситуацию и в таком случае запустить сперва git clone:
    def deploy():
        code_dir = '/srv/django/myproject'
        with settings(warn_only=True):
            if run("test -d %s" % code_dir).failed:
                run("git clone user@vcshost:/path/to/repo/.git %s" % code_dir)
        with cd(code_dir):
            run("git pull")
            run("touch app.wsgi")
Как и в случае с local выше, run так же позволяет нам строить логические выражения, основанные на выполнении команд оболочки. Самая интересная часть тут - вызов git clone. Так как мы используем SSH метод Git'a для доступа к репозиторию на нашем сервере Git, это означает, что вызов run должен аутентифицировать себя.
Старые версии Fabric (и схожие высокоуровневые библиотеки SSH) запускали удалённые программы в limbo и не могли подключиться к ней с локальной машины. Это проблематично, особенно когда Вам нужно ввести пароль или как-то ещё взаимодействовать с удалённо запущенной программой.
Fabric 1.0 и более поздние версии решают эти проблемы и позволяют Вам связаться с программой на удалённой машине. Давайте посмотрим, что происходит, когда мы запускаем нашу задачу deploy на новом сервере:
    $ fab deploy
    No hosts found. Please specify (single) host string for connection: my_server
    [my_server] run: test -d /srv/django/myproject

    Warning: run() encountered an error (return code 1) while executing 'test -d /srv/django/myproject'

    [my_server] run: git clone user@vcshost:/path/to/repo/.git /srv/django/myproject
    [my_server] out: Cloning into /srv/django/myproject...
    [my_server] out: Password: <enter password>
    [my_server] out: remote: Counting objects: 6698, done.
    [my_server] out: remote: Compressing objects: 100% (2237/2237), done.
    [my_server] out: remote: Total 6698 (delta 4633), reused 6414 (delta 4412)
    [my_server] out: Receiving objects: 100% (6698/6698), 1.28 MiB, done.
    [my_server] out: Resolving deltas: 100% (4633/4633), done.
    [my_server] out:
    [my_server] run: git pull
    [my_server] out: Already up-to-date.
    [my_server] out:
    [my_server] run: touch app.wsgi

    Done.
Обратите внимание на запрос пароля (Password) - git, запущенный на нашем сервере спрашивает у нас пароль к серверу Git. И мы можем вручную ввести его, после чего клонирование пройдёт нормально.

Как объявить подключения заранее

Указание адреса хоста для подключения во время выполнения не самое удобное решение, так что Fabric предоставляет несколько способов сделать это либо в fabfile, либо в командной строке. Мы не будем говорить обо всех этих способах здесь, но мы покажем наиболее популярный из них - установку глобального списка хостов, env.hosts.
env - глобальный объект, похожий на словарь, определяющий многие из настроек Fabric, и в него можно заносить свои значения (на самом деле, settings, о котором мы говорили выше, всего лишь обёртка над этим объектом). Таким образом, мы можем изменить его на уровне модуля вверху fabfile:
    from __future__ import with_statement
    from fabric.api import *
    from fabric.contrib.console import confirm

    env.hosts = ['my_server']

    def test():
        do_test_stuff()
Когда fab загружает наш fabfile, исполняется наша модификация env и она сохраняет наше изменение настройки. Конечный результат будет аналогичным - наша задача deploy будет запущена на сервере my_server.
Таким образом, мы можем легко сказать Fabric выполнить задачу сразу на нескольких серверах: так как env.hosts - это список, fab проходит по нему, вызывая задачу для каждого узла.

Заключение

Итак, наш fabfile всё ещё не большой, как и должно быть. Вот его содержание:
    from __future__ import with_statement
    from fabric.api import *
    from fabric.contrib.console import confirm

    env.hosts = ['my_server']

    def test():
        with settings(warn_only=True):
            result = local('./manage.py test my_app', capture=True)
        if result.failed and not confirm("Tests failed. Continue anyway?"):
            abort("Aborting at user request.")

    def commit():
        local("git add -p && git commit")

    def push():
        local("git push")

    def prepare_deploy():
        test()
        commit()
        push()

    def deploy():
        code_dir = '/srv/django/myproject'
        with settings(warn_only=True):
            if run("test -d %s" % code_dir).failed:
                run("git clone user@vcshost:/path/to/repo/.git %s" % code_dir)
        with cd(code_dir):
            run("git pull")
            run("touch app.wsgi")
Зато он использует большую часть возможностей Fabric:
  • определяет задачи в fabfile и выполняет их при помощи fab
  • вызывает локальные команды оболочки при помощи local
  • модифицирует переменную env при помощи settings
  • обрабатывает ошибки выполнения команд, запрашивает ввод от пользователя и может вручную прервать выполнение
  • определяет список хостов и запускает удалённые команды на них при помощи run
Однако, есть ещё много тем, которые мы ещё не покрыли. Обратитесь к ссылкам в разделах "см также" и к содержанию документации.
Спасибо за внимание!

Комментариев нет:

Отправить комментарий