Расширяем функционал Ansible с помощью плагинов: часть 1

ti6dfzt_wksiiposord9cnscw7c.jpeg

У себя в D2C мы активно используем Ansible. С его помощью мы создаем виртуальные машины у облачных провайдеров, устанавливаем программное обеспечение, а также управляем Docker-контейнерами с приложениями клиентов. В прошлой статье я рассказывал о том, как заставить Ansible работать быстрее, теперь расскажу о том, как расширить его функциональность.


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


Рассмотрим пример:


---
- hosts: localhost
  vars:
    foo:
      - a
      - b
      - c
  tasks:
    - copy:
        content: "{{ foo | shuffle }}"
        dest: /tmp/test


В данном случае copy — это модуль. Он будет выполнен на целевой машине; shuffle — Jinja2 фильтр, загруженный плагином. Плагины в Ansible выполняют не только видимую, но и скрытую от глаз работу.


Важно: все плагины в Ansible выполняются в контексте локального хоста (т.е. машины управления). Одной из частых ошибок является попытка прочитать переменные окружения на целевом хосте с помощью lookup-плагина env:


- name: show current user
  shell: echo {{ lookup('env','USER') }}


В этом случае не важно на каком сервере выполняется задача, переменная USER будет равна значению, которое выставлено у процесса ansible на машине управления. Аналогично со всеми другими плагинами.


Виды плагинов

Перечислю виды плагинов в алфавитном порядке, для Ansible 2.3.x:


Action


Action-плагины используются в качестве «обвязки» (wrappers) для модулей. Они выполняются непосредственно перед отправкой модулей на исполнение на целевые хосты. Обычно их используют для предварительной подготовки данных или для пост-обработки результатов выполнения модуля.


В общем виде выполнение задачи для mymodule выглядит так:


  • На локальном компьютере запускается action-плагин mymodule;
  • Внутри плагина выполняются подготовительные операции;
  • Дается команда на запуск модуля
  • На удаленном компьютере запускается модуль mymodule;
  • Результат возвращается на локальный компьютер
  • Управление возвращается в action-плагин mymodule;
  • Выполняется пост-обработка результатов


Если action-плагина mymodule не существует, используется базовый класс плагина.


Cache


Cache-плагины используются для организации хранилища (бэкендов) фактов. По умолчанию используется memory бэкенд, поэтому факты сохраняются только во время выполнения плейбука. Альтернативные бэкенды, доступные «из коробки»: jsonfile, memcached, pickle, redis, yaml.


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


Callback


Callback-плагины предоставляют возможность реагировать на события, которые генерирует Ansible во время выполнения плейбука. Например, вывод журнала работы Ansible на экран делается callback-плагином default, который реагирует на множество событий и выводит на экран, что происходит. Можно включить callback-плагин slack и получать информацию о ходе выполнения плейбука в канал в Slack.


Connection


Connection-плагины предоставляют различные способы подключения к целевым хостам — например, ssh — для Unix, winrm — для Windows, docker — для запуска модулей внутри контейнеров. Самые распространённые — ssh (по умолчанию) и local, который используется для запуска команд локально на машине управления.


Filter


Filter-плагины добавляют новые Jinja2 фильтры. Так как для работы с переменными в Ansible используется «движок» шаблонизации Jinja2, то в плейбуках доступны почти все его возможности, в том числе встроенные и дополнительные фильтры. Если требуются нестандартные фильтры, их можно добавить своими плагинами.


Lookup


Lookup-плагины используются для поиска или загрузки данных из внешних источников, а также для создания циклов.


Например, для загрузки значения из etcd можно использовать {{ lookup('etcd', 'foo') }}.


Чтобы сделать цикл по строчкам вывода команды, можно использовать плагин lines:


- debug:
    msg: "{{ item }}"
  with_lines: cat /etc/passwd


В этой задаче выполнится команда cat /etc/passwd (на локальном компьютере) и для каждой из строк вывода выплонится debug.


Для создания цикла можно использовать любой lookup-плагин в конструкции with_:. Когда вы делаете самый примитивный цикл with_items, вызывается lookup-плагин items.


Список доступных плагинов удобнее всего смотреть в репозитории (обращайте внимание на версию — данная ссылка для ветки 2.3.х).


Shell


Shell-плагины позволяют учитывать нюансы разного поведения оболочек на целевых устройствах. Например, bash или csh. Для Windows используется плагин powershell.


Strategy


Strategy-плагины определяют ход выполнения задач на целевых хостах. «Из коробки» доступны три плагина:


  • linear (включен по умолчанию) — Ansible выполняет текущую задачу на всех хостах по очереди, только после её выполнения на всех хостах переходит к следующей задаче
  • free — задачи выполняются на каждом из хостов так быстро, как это возможно, а не дожидаются всех хостов
  • debug — модификация linear — в случае ошибки включается интерактивная оболочка, которая позволяет просматривать текущие переменные, вносить изменения в параметры задачи и запускать её повторно (документация)


Terminal


Terminal-плагины позволяют учитывать разновидности интерактивных сред. Данные плагины используются для сетевых устройств типа коммутаторов и роутеров, так как работа с оболочкой на этих устройствах значительно отличается от работы полноценного shell’а на компьютере.


Test


Test-плагины добавляют Jinja2 тесты, которые используются в условных конструкциях. Аналогично фильтрам есть встроенные и дополнительные тесты.


Vars


Vars-плагины используются для манипуляции с переменными хостов (host vars, group vars) — встречаются крайне редко.


Пишем свои плагины

Приведу несколько примеров плагинов в порядке возрастания сложности.


Test


Например, мы часто работаем со спискам серверов EC2 и необходимо выбирать из списка инстансов те, которые работают. Можно использовать выражение:


{{ ec2.instances | selectattr('state','equalto','running') | list }}


Или написать свой test-плагин (положить в ./test_plugins/ec2.py):


class TestModule:
    def tests(self):
        return {
            'ec2_running': lambda i: i['state'] == 'running'
        }


И уже использовать:


{{ ec2.instances | select('ec2_running') | list }}


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


Аналогично можно использовать свои тесты внутри when:


when: my_instance | ec2_running


Задача будет выполнена, если my_instance в состоянии running.


Можно создавать тесты с параметром. Пример — стандартный тест divisibleby, который проверяет делится ли чисто на какое-то другое.


Filter


Фильтры используются для модификации переменных. Например, в Ansible очень долго не было механизмов работы с датой. Если вам нужно в плейбуках принимать решения на основе значений времени, вы можете использовать такой фильтр (положить в ./filter_plugins/add_date.py):


import datetime
class FilterModule(object):
    def filters(self):
        return {
            'add_time': lambda dt, **kwargs: dt + datetime.timedelta(**kwargs)
        }


Теперь в плейбуках можно «заглядывать» в будущее:


- debug:
    msg: "Current time +20 mins {{ ansible_date_time.iso8601[:19] | to_datetime(fmt) | add_time(minutes=20) }}"
  vars:
    fmt: "%Y-%m-%dT%H:%M:%S"


Action


Action-плагины удобно использовать, когда нужно немного модифицировать данные поступающие в модули или из него, или когда нужно выполнять какую-то задачу всегда локально на сервере управления. Пример — модуль debug для вывода информации, на самом деле не модуль так как никогда не копируется на удаленный хост, а существует лишь в виде action-плагина.


Чтобы показать, как работают action-плагины, модифицируем поведение модуля setup, который используется для сбора фактов. Его удобно использовать в качестве ad-hoc команды, чтобы посмотреть информацию о серверах:


ansible all -i myinventory -m setup


У этого модуля есть параметр filter, которым можно отфильтровать результат. Но у него есть одна особенность — он применяется только к ключам верхнего уровня. Если нам нужно проверить на серверах только временную зону, мы не можем указать tz. Или если нам нужно увидеть все ipv4 адреса мы не можем сделать фильтр по всем таким полям.


Добавим обертку в виде action-плагина (положить в ./action_plugins/setup.py):


from ansible.plugins.action import ActionBase
class ActionModule(ActionBase):
  def run(self, tmp=None, task_vars=None):
    def filter_dict(obj, filter):
        res = dict()
        for k, v in obj.items():
            if filter in k:
                res[k] = v
            elif isinstance(v, dict):
                val = filter_dict(v, filter)
                if val is not None and val != dict():
                    res[k] = val
        return res
    result = super(ActionModule, self).run(tmp, task_vars)
    query = self._task.args.get('query', None)
    module_args = self._task.args.copy()
    if query:
      module_args.pop('query')
    module_return = self._execute_module(module_name='setup',
                                         module_args=module_args,
                                         task_vars=task_vars, tmp=tmp)
    if not module_return.get('failed') and query:
      return dict(ansible_facts=filter_dict(module_return['ansible_facts'], query))
    else:
      return module_return


Минимально необходимая реализация плагина — наследование от ActionBase и описание метода run.


В нашем примере мы:


  • Определили вспомогательную функцию filter_dict, которая берет на вход объект (словарь), ищет в нем ключи по нашему фильтру и возращает объект только с тем ключами, которые удовлетворяют фильру (не важно на каком уровне вложенности они встретились);
  • Выполнили метод run родительского класса;
  • Попытались получить значение нашего нового параметра query: если он есть, запомнили его и удалили из списка параметров, которые передадим дальше модулю — иначе Ansible скажет, что модуль setup не знает ничего о параметре query и выдаст ошибку;
  • Передали управление модулю setup;
  • Если модуль успешно завершил работу и был задан паремтр query — фильтруем результат нашей функцией filter_dict, иначе возвращаем результат без именений;


Теперь мы добавили новый функционал в существующий модуль и при этом не трогали его код. С выходом новых версий Ansible модуль setup может научиться делать что-то ещё, но наш плагин по прежнему будет работать поверх этих возможностей.


Callback


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


Список callback-плагинов, доступных «из коробки», можно посмотреть в репозитории.


Для уведомлений, к примеру, доступны: mail, slack, hipchat.


Для модификации протоколирования, например: minimal, json. Для задания стандартного плагина вывода можно использовать настройку:


[defaults]
stdout_callback = json


Теперь Ansible не будет выводить человеко-читабельный протокол по ходу выполнения, а в самом конце выполнения плейбука выдаст огромный JSON со всей информацией. Его можно использовать для автоматического анализа результата в cron-задачах или на вашем сервере CI/CD.


Например, можно запустить плейбук и посчитать количество хостов, в которых были изменения:


ANSIBLE_STDOUT_CALLBACK=json ansible-playbook myplaybook.yml | jq '.stats | map(select(.changed > 0)) | length'


В качестве примера callback-плагина приведу «оповещалку» о выполнении плейбука, которая выводит уведомление в вашей графической оболочке по результатам выполнения плейбука (положить в ./callback_plugins/notify_me.py):


from ansible.plugins.callback import CallbackBase
from subprocess import call
from platform import system as get_system_name

class CallbackModule(CallbackBase):

    CALLBACK_VERSION = 2.0
    CALLBACK_TYPE = 'notification'
    CALLBACK_NAME = 'notify_me'
    CALLBACK_NEEDS_WHITELIST = True

    def v2_playbook_on_stats(self, stats):

        def notify(msg,is_error=False):
            sys_name = get_system_name()
            if sys_name == 'Darwin':
                sound = "Basso" if is_error else "default"
                call(["osascript", "-e",
                    'display notification "{}" with title "Ansible" sound name "{}"'.
                    format(msg,sound)])
            elif sys_name == 'Linux':
                icon = "dialog-warning" if is_error else "dialog-info"
                rc = call(["notify-send", "-i", icon, "Ansible", msg])
                print "error code {}".format(rc)

        hosts = stats.processed.keys()
        failed_hosts = []

        for h in hosts:
            t = stats.summarize(h)
            if t['unreachable'] + t['failures'] > 0:
                failed_hosts.append(h)

        if len(failed_hosts) > 0:
            notify("Failed hosts: {}".format(" ".join(failed_hosts)),True)
        else:
            notify("Job's done!")


В плагине предусмотрена некая попытка кросс-плаформенности :)


Мы наследуемся от класса CallbackBase и переопределяем метод v2_playbook_on_stats, который вызывается в момент готовности финального отчета о выполнении плейбука. Стандартный плагин протоколирования по этому методу формирует таблицу PLAY RECAP.


Нам понадобится вспомогательная функция notify, которая в зависимости от платформы пытается отправить пользователю оповещение.


В основном теле нашего метода мы проверяем есть ли хосты с ошибками: если есть — отправляем плохую нотификацию со списком хостов, если нет — отправляем хорошую нотификацию Job's done!.


Обратите внимание на CALLBACK_NEEDS_WHITELIST = True. Этот параметр говорит Ansible, что данный плагин требует принудительного включения. То есть, не смотря на готовность плагина к работе, он будет включен лишь при добавлении его в whitelist. Это сделано, чтобы при работе с плейбуками экран не замусоривался, но можно было легко поставить такое уведомление для «долгоиграющих» плейбуков, которые вы запускаете в фоне и идете заниматься другим делом. Проверить работу можно так:


ANSIBLE_CALLBACK_WHITELIST=notify_me ansible-playbook test.yml


Полный список методов (событий), которые можно переопределять в callback-плагинах, лучше всего посмотреть в исходном коде.


--


Можете смело экспериментировать с примерами из статьи. Ещё несколько видов плагинов рассмотрим в следующей части. Stay tuned!

© Habrahabr.ru