Под капотом сервиса d2c.io мы активно используем Ansible – от создания виртуальных машин в облаках провайдеров и установки необходимого программного обеспечения, до управления Docker-контейнерами с приложениями клиентов.
В первой части мы рассмотрели типы плагинов, которые поддерживает Ansible и сделали несколько своих плагинов: test, filter, action и callback. В этой статье попробуем более сложные модификации.
Callback с «мутацией»
Самым частым применением callback-плагинов являются системы протоколирования и оповещения. Однако, с их помощью можно не только выполнять пассивное наблюдения за событиями, но еще и активно влиять на ход выполнения плейбука.
Чтобы иметь возможность выполнять только некоторые задачи из каких-то ролей, мы в D2C активно используем теги. Например, при запуске роли с тегом build
произойдет полная сборка сервиса «с нуля», а при запуске с тегом update-configs
– лишь обновление файлов конфигурации и их применение. В варианте «из коробки» Ansible может применять единый набор тегов ко всему плейбуку.
Разберем задачу запуска Master-Slave репликации для MySQL сервиса:
- необходимо обновить конфигурацию основного сервера
- сделать копию базы для первичного наполнения реплики
- сделать второй сервер, настроить репликацию
- восстановить оригинальную базу на реплике
- удалить временные данные
Каждая из задач имеет свои теги. Чтобы объединить этот процесс в один плейбук мы можем описать три плея (play – единица конфигурации из множества которых состоит playbook): для подготовки мастера, для подготовки реплики, для очистки. Однако, мы не можем указать теги для каждой части по отдельности, так как они задаются через параметр tags
для всего плейбука целиком. Давайте исправим это, воспользовавшись callback-плагином:
from ansible.plugins.callback import CallbackBase
from ansible.parsing.yaml.objects import AnsibleUnicode
from ansible.compat.six import string_types
import json
import os
class CallbackModule(CallbackBase):
CALLBACK_VERSION = 2.0
CALLBACK_NAME = 'use_tags'
def __init__(self):
super(CallbackModule, self).__init__()
self.tmp_context = None
self.warn = False if os.environ.get('ANSIBLE_D2C_NO_WARN') else True
def v2_playbook_on_play_start(self, play):
vm = play.get_variable_manager()
extra_vars = vm.extra_vars
enable_use_tags = False
if 'enable_use_tags' in extra_vars:
if extra_vars['enable_use_tags']:
enable_use_tags = True
play_vars = vm.get_vars(play._loader, play=play)
if enable_use_tags:
tags = self.tmp_context.only_tags
tags.clear()
if 'use_tags' in play_vars:
use_tags = play_vars['use_tags']
if isinstance(use_tags, (string_types, AnsibleUnicode)):
use_tags = [t.strip() for t in use_tags.split(',')]
if isinstance(use_tags, list):
for t in use_tags:
tags.add(t)
else:
tags.add('all')
self._display.display(' [INFO]: "use_tags" variable is set, but unparsable (type "{}" is not a list or a string): {}'.format(type(use_tags),use_tags), color='cyan')
else:
self._display.display(' [INFO]: "use_tags" variable is not set, but "enable_use_tags" is set', color='cyan')
tags.add('all')
if self.warn:
self._display.warning('Tags modified to: {}'.format(json.dumps(list(tags))))
def set_play_context(self, play_context):
self.tmp_context = play_context
В нашем плагине главный герой – метод v2_playbook_on_play_start
. Он вызывается после инициализации плея (наполнения переменными, определения списка хостов и пр.) и перед началом выполнения самих задач (tasks).
Мы используем дополнительную переменную (extra var) enable_use_tags
как признак того, что будем использовать модификацию тегов «на лету» и переменную уровня плея (play var) use_tags
для формирования списка необходимых тегов.
Всё бы хорошо, но теги вместе со множеством другой runtime информации во время инициализации копируются в объект PlayContext
, ссылка на который отстутствует в методе v2_playbook_on_play_start
. Для борьбы с этим заметим, что менеджер очереди в Ansible проверяет наличие метода set_play_context
в подключенных плагинах и, если он есть, вызывает его, передавая этот самый контекст.
Используя те обстоятельства, что PlayContext
изменяемый (mutable) и что Ansible одновременно работает только с одним плеем (play) реализуем следующий алгоримт в плагине:
- при первичной инициализации плагина обнуляем
tmp_context
внутри плагина - при каждом вызове
set_play_context
запоминаем текущий контект вtmp_context
- при последующем вызове
v2_playbook_on_play_start
анализируем переменныеenable_use_tags
иuse_tags
и изменяем оригинальный объектPlayContext
(точнее получаем «ссылку» на mutable список тегов черезself.tmp_context.only_tags
и модифицируем список) - выводим соответствующие предупреждения, что список тегов изменен (чтобы не было неожиданностей для пользователя)
Теперь мы можем запустить такой плейбук:
ansible-playbook -e enable_use_tags=1 make_mysql_slave.yml
- hosts: master
vars:
use_tags: update-configs, replication-init, replication-sync
roles:
- mysql
- hosts: slave
vars:
use_tags: build, replication-init, replication-sync
roles:
- mysql
- hosts: all
vars:
use_tags: replication-sync-cleanup
roles:
- mysql
В этом случае Ansible будет использовать для каждого плея (play) свой набор тегов. Это дает нам возможность компановать оркестрацию сложных конфигураций едиными плейбуками.
Connection
Connection плагины используется для организации соединения с целевыми хостами. Вкратце: плагин должен предоставлять возможность установить и разорвать соединение, отправить файл, запустить удаленную команду. Примерами плагинов «из коробки» являются: local
, ssh
(используется по умолчанию), winrm
, docker
.
Если у вас совершенно особенные целевые хосты, например, какая-нибудь проприетарная система виртуализации, то вам придется написать свой плагин с нуля. Но если вам нужно добавить немного функционала к существуюему, можно унаследоваться от плагина «из коробки» и переопределить необходимые методы.
Рассмотрим пример SSH-подключения с использованием port knoking. В основном эти ssh-сессии ничем не отличаются от обычных, но перед попыткой подключения к удаленной машине необходимо «постучать» на определенные порты, чтобы сервер открыл 22 порт и принял ssh-соединение.
Доработаем базовый плагин ssh
(положить в ./connection_plugins/ssh_pkn.py):
from ansible.plugins.connection.ssh import Connection as ConnectionSSH
from ansible.errors import AnsibleError
from socket import create_connection
from time import sleep
try:
from __main__ import display
except ImportError:
from ansible.utils.display import Display
display = Display()
class Connection(ConnectionSSH):
def __init__(self, *args, **kwargs):
super(Connection, self).__init__(*args, **kwargs)
display.vvv("SSH_PKN (Port KNock) connection plugin is used for this host", host=self.host)
def set_host_overrides(self, host, hostvars=None):
if 'knock_ports' in hostvars:
ports = hostvars['knock_ports']
if not isinstance(ports, list):
raise AnsibleError("knock_ports parameter for host '{}' must be list!".format(host))
delay = 0.5
if 'knock_delay' in hostvars:
delay = hostvars['knock_delay']
for p in ports:
display.vvv("Knocking to port: {0}".format(p), host=self.host)
try:
create_connection((self.host, p), 0.5)
except:
pass
display.vvv("Waiting for {0} seconds after knock".format(delay), host=self.host)
sleep(delay)
Мы используем метод set_host_overrides
, который дает возможность плагинам изменять свое поведение в зависимости от host/group переменных. Этот метод вызывается при создании нового соединения когда не используется reuse. В нашем случае он не должен лишний раз «простукивать» порты.
Пример inventory файла для использования данного плагина:
[pkn]
myserver ansible_host=my.server.at.example.com
[pkn:vars]
ansible_connection=ssh_pkn
knock_ports=[8000,9000]
knock_delay=2
Мы указали, что для всех хостов в группе pkn
будет использоваться connection-плагин ssh_pkn
. При инициализации нашего плагина внутри метода set_host_overrides
сработает условие, что определена переменная knock_ports
. Затем для каждого из портов в списке будет выполнена попытка соединения с интервалом knock_delay
в 2 секунды. Мы также перехватываем все исключения от create_connection
, так как скорее всего порты для «простукивания» закрыты и попытки соединения будут безуспешными. Однако для нас это не особо важно – сервер в любом случае увидит попытки.
Strategy
Плагины типа strategy определяют порядок запуска задач (tasks) и выполняют множество «подкапотной работы»: в том числе динамическое добавление фактов, отслеживание состояния хостов (healty/failed/unreachable) и вызов callback'ов. О strategy-плагинах «из коробки» я писал поробнее в первой части.
Такие пользовательские плагины встречаются крайне редко. В непринятом pull-реквесте 18460, например, предлагали плагин с возожностью инъекции задач в произвольное место плейбука, чтобы повысить гибкость распространяемых ролей. Мы же сделаем более приземпленный strategy-плагин.
Положить в ./strategy_plugins/step_critical.py:
from ansible.plugins.strategy.linear import StrategyModule as LinearStrategyModule
import os
try:
from __main__ import display
except ImportError:
from ansible.utils.display import Display
display = Display()
class StrategyModule(LinearStrategyModule):
def __init__(self, tqm):
super(StrategyModule, self).__init__(tqm)
display.vv('Safenet strategy: will give a prompt at critical tasks!')
force_step = os.environ.get('ANSIBLE_FORCE_STEP', None)
if force_step and force_step.lower() in ['1','y','yes','true','on']:
display.vv('Safenet: "step" option is forced via environment!')
self._step = True
def _take_step(self, task, host=None):
v = task.get_vars()
ret = True
if 'is_critical' in v:
if v['is_critical']:
display.vv('Safenet: critical task detected!')
return super(StrategyModule, self)._take_step(task, host)
return ret
Этот плагин изменяет поведение параметра --step
таким образом, что Ansible спрашивает разрешение только для задач, у которых определена переменная is_critical
и её значение True
, а не для всех подряд, как это происходит «из коробки».
Также мы можем принудительно включить режим подтверждения через переменную окружения ANSIBLE_FORCE_STEP
, а не только через параметр --step
. В остальном данный плагин наследует поведение плагина linear
.
Проверить поведение плагина вы можете следующим плейбуком:
---
- hosts: localhost
strategy: step_critical
gather_facts: no
tasks:
- name: Ensure user exists
debug:
msg: user_module
- name: Drop database
debug:
msg: db_module
vars:
is_critical: yes
- name: Ensure permissions
debug:
msg: permission_module
Итого
В двух статьях о возможностях расширения Ansible мы рассмотрели все типы плагинов, которые поддерживаются в версии Ansible 2.3. А также я привел примеры для большинства из них.
Если какие-то вопросы о плагинах остались не раскрытыми, пишите в комментариях – постараюсь ответить.
А пока я приступаю к подготовке статьи о создании модулей для Ansible. Stay tuned!
Автор: Berlic