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


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

    D2C.io

    33,00

    Компания

    Поделиться публикацией

    Похожие публикации

    Комментарии 0

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое