Расширяем функционал 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-объекта в файл. Используется для модулей на интерпретируемых языках, в теле которых есть маркер
<
. Перед отправкой модуля на целевой хост эта строка будет замена на 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!