Расширяем функционал Ansible с помощью плагинов: часть 1
У себя в 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!