- PVSM.RU - https://www.pvsm.ru -

У себя в D2C [1] мы активно используем Ansible. С его помощью мы создаем виртуальные машины у облачных провайдеров, устанавливаем программное обеспечение, а также управляем Docker-контейнерами с приложениями клиентов. В прошлой статье я рассказывал о том, как заставить Ansible работать быстрее [2], теперь расскажу о том, как расширить его функциональность.
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-плагины используются в качестве «обвязки» (wrappers) для модулей. Они выполняются непосредственно перед отправкой модулей на исполнение на целевые хосты. Обычно их используют для предварительной подготовки данных или для пост-обработки результатов выполнения модуля.
В общем виде выполнение задачи для mymodule выглядит так:
mymodule; mymodule; mymodule; Если action-плагина mymodule не существует, используется базовый класс плагина.
Cache-плагины используются для организации хранилища (бэкендов) фактов. По умолчанию используется memory бэкенд, поэтому факты сохраняются только во время выполнения плейбука. Альтернативные бэкенды, доступные «из коробки»: jsonfile, memcached, pickle, redis, yaml.
Кэш фактов используется, если необходимо работать со множеством удалённых хостов и сбор фактов занимает продолжительное время. В такой ситуации можно по расписанию актуализировать кэш, а в самих плейбуках сбор фактов не выполнять или выполнять в редких случаях принудительной командой.
Callback-плагины предоставляют возможность реагировать на события, которые генерирует Ansible во время выполнения плейбука. Например, вывод журнала работы Ansible на экран делается callback-плагином default, который реагирует на множество событий и выводит на экран, что происходит. Можно включить callback-плагин slack и получать информацию о ходе выполнения плейбука в канал в Slack.
Connection-плагины предоставляют различные способы подключения к целевым хостам – например, ssh – для Unix, winrm – для Windows, docker – для запуска модулей внутри контейнеров. Самые распространённые – ssh (по умолчанию) и local, который используется для запуска команд локально на машине управления.
Filter-плагины добавляют новые Jinja2 фильтры. Так как для работы с переменными в Ansible используется «движок» шаблонизации Jinja2 [3], то в плейбуках доступны почти все его возможности, в том числе встроенные [4] и дополнительные [5] фильтры. Если требуются нестандартные фильтры, их можно добавить своими плагинами.
Lookup-плагины используются для поиска или загрузки данных из внешних источников, а также для создания циклов.
Например, для загрузки значения из etcd можно использовать {{ lookup('etcd', 'foo') }}.
Чтобы сделать цикл по строчкам вывода команды, можно использовать плагин lines:
- debug:
msg: "{{ item }}"
with_lines: cat /etc/passwd
В этой задаче выполнится команда cat /etc/passwd (на локальном компьютере) и для каждой из строк вывода выплонится debug.
Для создания цикла можно использовать любой lookup-плагин в конструкции with_<plugin-name>:. Когда вы делаете самый примитивный цикл with_items, вызывается lookup-плагин items.
Список доступных плагинов удобнее всего смотреть в репозитории [6] (обращайте внимание на версию – данная ссылка для ветки 2.3.х).
Shell-плагины позволяют учитывать нюансы разного поведения оболочек на целевых устройствах. Например, bash или csh. Для Windows используется плагин powershell.
Strategy-плагины определяют ход выполнения задач на целевых хостах. «Из коробки» доступны три плагина:
linear (включен по умолчанию) – Ansible выполняет текущую задачу на всех хостах по очереди, только после её выполнения на всех хостах переходит к следующей задачеfree – задачи выполняются на каждом из хостов так быстро, как это возможно, а не дожидаются всех хостовdebug – модификация linear – в случае ошибки включается интерактивная оболочка, которая позволяет просматривать текущие переменные, вносить изменения в параметры задачи и запускать её повторно (документация [7])Terminal-плагины позволяют учитывать разновидности интерактивных сред. Данные плагины используются для сетевых устройств типа коммутаторов и роутеров, так как работа с оболочкой на этих устройствах значительно отличается от работы полноценного shell'а на компьютере.
Test-плагины добавляют Jinja2 тесты, которые используются в условных конструкциях. Аналогично фильтрам есть встроенные [8] и дополнительные [9] тесты.
Vars-плагины используются для манипуляции с переменными хостов (host vars, group vars) – встречаются крайне редко.
Приведу несколько примеров плагинов в порядке возрастания сложности.
Например, мы часто работаем со спискам серверов 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 [10], который проверяет делится ли чисто на какое-то другое.
Фильтры используются для модификации переменных. Например, в 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-плагины удобно использовать, когда нужно немного модифицировать данные поступающие в модули или из него, или когда нужно выполнять какую-то задачу всегда локально на сервере управления. Пример — модуль debug для вывода информации, на самом деле не модуль так как никогда не копируется на удаленный хост, а существует лишь в виде action-плагина.
Чтобы показать, как работают action-плагины, модифицируем поведение модуля setup [11], который используется для сбора фактов. Его удобно использовать в качестве 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-плагины используются для того, чтобы следить за событиями, которые происходят внутри Ansible во время выполнения плейбука и как-то реагировать на них. Одним из самых частых использований таких плагинов являются протоколирование, логгирование и оповещение.
Список callback-плагинов, доступных «из коробки», можно посмотреть в репозитории [12].
Для уведомлений, к примеру, доступны: 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-плагинах, лучше всего посмотреть в исходном коде [13].
--
Можете смело экспериментировать с примерами из статьи. Ещё несколько видов плагинов рассмотрим в следующей части. Stay tuned!
Автор: Berlic
Источник [14]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/servernoe-administrirovanie/270081
Ссылки в тексте:
[1] У себя в D2C: https://d2c.io/
[2] как заставить Ansible работать быстрее: https://habrahabr.ru/company/d2cio/blog/343368/
[3] Jinja2: http://jinja.pocoo.org/
[4] встроенные: http://jinja.pocoo.org/docs/2.9/templates/#list-of-builtin-filters
[5] дополнительные: http://docs.ansible.com/ansible/latest/playbooks_filters.html
[6] репозитории: https://github.com/ansible/ansible/tree/stable-2.3/lib/ansible/plugins/lookup
[7] документация: http://docs.ansible.com/ansible/latest/playbooks_debugger.html
[8] встроенные: http://jinja.pocoo.org/docs/2.9/templates/#list-of-builtin-tests
[9] дополнительные: http://docs.ansible.com/ansible/latest/playbooks_tests.html
[10] divisibleby: http://jinja.pocoo.org/docs/2.9/templates/#divisibleby
[11] setup: http://docs.ansible.com/ansible/latest/setup_module.html
[12] в репозитории: https://github.com/ansible/ansible/tree/stable-2.3/lib/ansible/plugins/callback
[13] исходном коде: https://github.com/ansible/ansible/blob/stable-2.3/lib/ansible/plugins/callback/__init__.py#L283
[14] Источник: https://habrahabr.ru/post/344046/
Нажмите здесь для печати.