Расширяем функционал Ansible с помощью плагинов: часть 2

в 7:15, , рубрики: Ansible, devops, infrastructure as code, Блог компании D2C.io, Серверное администрирование

Расширяем функционал Ansible с помощью плагинов: часть 2 - 1

Под капотом сервиса 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

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js