Расширяем функционал Ansible с помощью модулей

    Под капотом сервиса d2c.io мы активно используем Ansible – от создания виртуальных машин в облаках провайдеров и установки необходимого программного обеспечения, до управления Docker-контейнерами с приложениями клиентов.


    В статье о раширении функциональности Ansible мы частично рассмотрели, чем отличаются плагины от модулей. Если вкратце, основное различие в том, что первые выполняются на локальной машине, где установлен Ansible, а вторые — на целевых.


    Основная задача плагинов – влиять на ход выполнения плейбука, добавлять новые возможности загрузки и обработки данных. Задача же модулей – расширять перечень систем и сервисов, которыми Ansible может управлять. Например, создать сервер на площадке Vultr – модуль vultr, создать пользователя в самодельной системе авторизации для офисной WiFi сети – модуль mywifiauth_user.


    Принцип работы модулей


    Модуль – это небольшая программа, которая:


    • исполняется на целевом хосте
    • может принимать на вход параметры (через файл параметров)
    • выдает отчёт о своей работе на стандартный вывод в JSON формате

    Процесс исполнения модуля выглядит так:


    • Из очереди исполнения Ansible берет следующую задачу. Определяет название модуля, который нужно использовать.
    • Если существует одноименный action-плагин, выполняет его (см. статью о плагинах ч.1).
      Плагин может выполнить подготовительную работу. Например, заранее скопирует на целевой хост файлы с машины управления.
    • Подготавливает файл параметров для модуля
    • В зависимости от типа модуля:
      • для модулей, которые не используют «Ansible Framework»: копирует файл параметров и исполняемый файл модуля на целевой хост.
      • для модулей на основе AnsibleModule: формирует и доставляет на целевой хост самораспаковывающийся Python-файл, содержащий все необходимые вспомогательные классы Ansible, файл параметров и файл самого модуля. Такой «пакет» необходим для работы режима pipelining (см. статью про ускорение Ansible).
    • Запускает на удаленном хосте модуль или «пакет».
    • Модуль выполняет полезную работу.
    • Ansible получает результат работы модуля в виде JSON-объекта со стандартного вывода.

    Чаще всего целевыми хостами являются удаленные машины, серверы и устройства: например, модуль user управляет пользователями именно на том хосте, на котором запускается. Некоторые модули, наоборот, чаще запускаются на локальном хосте, используя connection: local, local_action или delegate_to: localhost. Яркий пример тому модули управления облачными ресурсами, такие как ec2. Они требуют настройки учётных данных, которые чаще всего доступны именно на машине управления. Модуль wait_for для ожидания открытия TCP-портов тоже часто запускается на локальной машине. Он используется, например, для ожидания пока удаленный сервер перезагрузится и SSH-подключение станет доступно.


    Простейший модуль


    Модули можно создавать на любом языке. Начиная с Ansible 2.2 модули могут быть бинарными исполняемыми файлами. Сделаем простейший модуль на bash:


    #!/bin/bash
    
    echo '{"changed":false,"date":"'$(date)'"}'

    Сохраните данный код в файл ./library/bash_mod.sh и проверьте:


    $ ansible localhost -m bash_mod
    localhost | SUCCESS => {
        "changed": false,
        "date": "среда, 6 сентября 2017 г. 17:00:32 (MSK)"
    }

    Ansible получает сведения о результатах работы модулей с их стандартного вывода. Весь вывод должен быть правильным JSON-объектом (поэтому нельзя отлаживать модули с помощью операторов print). В зависимости от значения служебных свойств Ansible может принимать разные решения. Одно из таких свойств: changed. Например, можете изменить в bash_mod его значение с false на true и увидеть, что Ansible теперь считает, что ваш модуль что-то изменил на целевом хосте, вывод стал желтого цвета.


    Входные параметры


    Есть несколько способов получать входные параметры для модуля:


    • Через файл с парами key=value, разделенных пробелами. Используется для модулей на интерпретируемых языках. Путь к файлу с параметрами передается модулю в качестве единственного параметра командной строки. Например, для нашего модуля на bash.
    • Через файл с JSON-объектом. Используется для бинарных модулей, модулей на основе AnsibleModule и модулей на интерпретируемых языках, в теле которых есть слово WANT_JSON. Например, в наш модуль на bash можно добавить комментарий # WANT_JSON.
    • Через иньекцию JSON-объекта в файл. Используется для модулей на интерпретируемых языках, в теле которых есть маркер <<INCLUDE_ANSIBLE_MODULE_JSON_ARGS>>. Перед отправкой модуля на целевой хост эта строка будет замена на JSON-объект с параметрами.

    Если вы создаете модуль на основе AnsibleModule, то заботиться о том, как передаются параметры необходимости нет – всю подкапотную работу сделает базовый класс.


    Класс AnsibleModule


    Для упрощения разработки пользовательских модулей в Ansible есть класс AnsibleModule. Он берет на себя обработку параметров, проверку их типов и допустимых значений, а также предоставляет набор вспомогательных методов работы с файлами, контрольными суммами и прочим.


    В качестве базового примера посмотрим на код модуля ping:


    from ansible.module_utils.basic import AnsibleModule
    
    def main():
        module = AnsibleModule(
            argument_spec=dict(
                data=dict(required=False, default=None),
            ),
            supports_check_mode=True
        )
        result = dict(ping='pong')
        if module.params['data']:
            if module.params['data'] == 'crash':
                raise Exception("boom")
            result['ping'] = module.params['data']
        module.exit_json(**result)
    
    if __name__ == '__main__':
        main()

    Описывается модуль, у которого единственный допустимый, но необязательный, параметр data. Он поддерживает режим проверки, ключ --check при запуске Ansible. В результате возвращает pong или значение параметра data. Если передать crash в качестве данных, модуль «упадет» с ошибкой.


    Для более сложных примеров можно обратиться к коду других модулей. В этом плане удобно, что Ansible – проект с открытым исходным кодом.


    Пример модуля


    Один из удобных случаев для написания модуля – обертка для shell-команд. Если какую-то операцию на целевом хосте вы можете сделать через командную строку с использованием модулей shell/command, но при этом вам её нужно использовать часто в разных плейбуках и вы хотите сделать код красивым и читабельным. Я разберу немного мифический, но от этого не менее рабочий, пример с настройкой уровня громкости в операционной системе:


    #!/usr/bin/python
    # -*- coding: utf-8 -*-
    
    DOCUMENTATION = '''
    ---
    module: osx_volume
    short_description: Set OS X volume level
    description:
       - Set OS X volume level or mute flag
    options:
        level:
            description:
                - Volume level to be applied
            aliases:
                - volume
            required: false
        muted:
            description:
                - Set mute on/off
            required: false
    author:
        - Konstantin Suvorov
    '''
    
    EXAMPLES = '''
    - name: Set volume to 25
      osx_volume:
        level: 25
    - name: Mute
      osx_volume:
        muted: yes
    '''
    
    from ansible.module_utils.basic import AnsibleModule
    from subprocess import call, check_output
    
    def get_volume():
        level = check_output(['osascript','-e','output volume of (get volume settings)']).strip()
        muted = check_output(['osascript','-e','output muted of (get volume settings)']).strip()
        muted = (muted.lower() == "true")
        return (int(level), muted)
    
    def set_volume(level=None, muted=None):
        if level is not None:
            call(['osascript','-e','set volume output volume {}'.format(level)])
        if muted is not None:
            mute_str = 'true' if muted else 'false'
            call(['osascript','-e','set volume output muted {}'.format(mute_str)])
        return get_volume()
    
    def main():
        module = AnsibleModule(
            argument_spec=dict(
                level=dict(type='int', required=False, default=None, aliases=['volume']),
                muted=dict(type='bool', required=False, default=None)
            ),
            supports_check_mode=True
        )
        req_level = module.params['level']
        req_muted = module.params['muted']
    
        l, m = get_volume()
        result = dict(level=(req_level if req_level is not None else l),
                      muted=(req_muted if req_muted is not None else m),
                      changed=False)
    
        if req_level is not None and l != req_level:
            result['changed'] = True
        elif req_muted is not None and m != req_muted:
            result['changed'] = True
    
        if module.check_mode or not result['changed']:
            module.exit_json(**result)
    
        new_l, new_m = set_volume(level=req_level, muted=req_muted)
    
        if req_level is not None and new_l != req_level:
            module.fail_json(msg="Failed to set requested volume level {} (actual {})!".format(req_level, new_l))
        if req_muted is not None and new_m != req_muted:
            module.fail_json(msg="Failed to set requested mute flag {} (actual {})!".format(req_muted, new_m))
    
        module.exit_json(**result)
    
    if __name__ == '__main__':
        main()

    Я не буду подробно разбирать код модуля – можете посмотреть его самостоятельно. Модуль настраивает уровень громкости, управляет режимом mute в MacOS. Поддерживает режим проверки (dry-run). Он выведет статус changed=true, если значения должны поменяться. Модуль идемпотентен, и если применить его два раза с одинаковыми параметрами, он ничего не будет делать и выведет статус changed=false.


    Отладка модулей


    Вариант без какой-либо подготовительной работы – запустить Ansible с включенной настройкой ANSIBLE_KEEP_REMOTE_FILES=1 и уровнем протоколирования -vvv. В этом случае Ansible не будет удалять сгенерированные файлы модуля и параметров, а оставит их во временной папке на целевом хосте. Расширенный уровень протоколирования позволит увидеть путь каталога, в котором лежат файлы.


    Заходим на целевой хост по SSH, переходим в нужную папку, например, cd /tmp/ansible-tmp-1488291604.43-129413612218427. Теперь можем локально запускать, изменять и отлаживать наш модуль. Если модуль написан на основе AnsibleModule, то его можно запустить с отладочными командами:


    • explode – распаковать «архив» в папку для дальнейшей модификации
    • execute – запуск модуля из полученной на предыдущем шаге папки

    К примеру, если мы отлаживаем таким образом модуль ping, то:


    • ./ping.py – запустить модуль из «пакета»
    • ./ping.py explode – распакует модуль и параметры в папку debug_dir
    • ./ping.py execute – запустить модуль из папкуи debug_dir (со всеми изменениями, которые мы там сделаем)

    Другой вариант – использование утилиты test-module. Она позволяет выполнять подготовку файла с параметрами и «упаковку» модуля аналогично тому, как это делает Ansible в реальной работе. Этот вариант позволяет тестировать модули локально и гораздо быстрее, чем через Ansible; проще подключить отладчик.


    Распространение модулей


    Чтобы Ansible «увидел» модуль – он должен находиться на локальной машине в путях поиска модулей. По умолчанию это каталог ./library рядом с плейбуком, но эту настройку можно изменить в конфигурационном файле или через переменные окружения.


    Если вы используете роли, внутри роли также может быть папка library с вашим модулем, например, ./roles/myrole/library/mymodule.py. В таком случае, если в плейбуке была применена роль myrole, то и mymodule станет доступен. Роль даже может быть пустой, без файла tasks/main.yml.


    Документирование модулей


    Модули полезно документировать! Если документация внутри модулей на Python описана в переменных DOCUMENTATION и EXAMPLES в соответствии с определенным форматом (см. пример выше), то информацию о вашем модуле можно будет удобно просматривать при помощи утилиты ansible-doc из стандартного пакета Ansible.


    Помимо справочной информации о модуле эта утилита может генерировать «сниппеты» для вставки в код плейбуков, например:


    $ ansible-doc -s postgresql_db
    - name: Add or remove PostgreSQL databases from a remote host.
      action: postgresql_db
          encoding               # Encoding of the database
          lc_collate             # Collation order (LC_COLLATE) to use in the data
          lc_ctype               # Character classification (LC_CTYPE) to use in t
          login_host             # Host running the database
          login_password         # The password used to authenticate with
          login_unix_socket      # Path to a Unix domain socket for local connecti
          login_user             # The username used to authenticate with
          name=                  # name of the database to add or remove
          owner                  # Name of the role to set as owner of the databas
          port                   # Database port to connect to.
          ssl_mode               # Determines whether or with what priority a secu
          ssl_rootcert           # Specifies the name of a file containing SSL cer
          state                  # The database state
          template               # Template used to create the database



    Что ж, пора завершать статью о модулях. Учитывая предыдущие статьи вы теперь можете расширять функционал Ansible во всех направлениях! Если остались вопросы, задавайте в комментариях – постараюсь ответить или подготовить ещё одну статью. Stay tuned!

    • +20
    • 6,5k
    • 2

    D2C.io

    33,00

    Компания

    Поделиться публикацией

    Похожие публикации

    Комментарии 2
      +1
      Отличный цикл статей, прямо must have для пользователей Ansible.
      Вопрос немного не по теме, насчёт модулей, которые запускаются локально («connection: local, local_action или delegate_to: localhost»). У нас в проекте возникает небольшая проблема с тем, что у нас есть роли, которые по своей сути должны запускать локальные таски на control machine (к примеру разворачивание виртуалок в облаке, деплой приложений через REST API в какой-нибудь Mesos/Marathon и т.д.). Поэтому в playbook мы пишем что-то вроде
      - hosts: localhost
      roles:
      - name: deploy-vm

      И это вынуждает нас записывать localhost в inventory, что само по себе логически не правильно, т.к. localhost не является единицей нашего боевого парка машин. А так же пришлось для localhost ещё и фиктивные host_vars написать, те же параметры, что и у «настоящих» хостов из inventory. Надеюсь не слишком сумбурно изложил. Есть ли у вас такая проблема и знаете ли вы её элегантное решение?
        0

        Нет необходимости добавлять localhost в инвентарь. Ansible умеет понимать что такое localhost и без добавления в инвентарь (см. код). Т.е. в инвентаре могут быть серверы test1,test2 и группа all будет содержать именно эти два сервера. Но при этом вы смело можете запустить какой-нибудь плей для localhost (- hosts: localhost) и Ansible выполнит его на локальном хосте.


        Что касается роли deploy-vm, то в зависимости от того, что внутри неё – можно делать разный подход. И либо целиком запускать на локалхосте, либо, как часто делают, в целой роли только первая задача делегируется на локалхост (сам процесс создания VM), а следующие таски выполняются уже на вновь созданной виртуальной машине (такие как hostname, mount и т.п.).

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое