- 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/
Нажмите здесь для печати.