Photo by Xavier von Erlach on Unsplash

В предыдущей статье, я рассказал, как можно упростить себе жизнь, воспользовавшись возможностями динамического инвентаря в виде скрипта. Это очень удобно, когда нужно вытянуть структуру хостов из какой-то системы со всеми нужными параметрами.

Но, как я уже тогда сказал, этот способ зашит в ansible для быстрых решений. Более продвинутый, гибкий и переиспользуемый метод - это написание собственного плагина. Но опять рассказывать про инвентарь мне стало очень скучно, поэтому в этот раз мы будем учиться подключаться и выполнять модули на целевом хосте.

Как обычно, мы будем рассматривать пример работоспособный в Ansible 2.9. Для других версий скорее всего изменения не потребуются или потребуются незначительные.

Дополнительная мотивация к написанию connection-плагина

Я не раз сталкивался с интересной ситуацией, когда в довольно крупных компаниях, где есть отдел безопасности (даже иногда в одном лице), зарезали на корню идею использования ansible. Повод был такой - ssh'ем торчать даже во внутреннюю сеть очень страшно, да и вообще не должен один хост иметь доступ по этому протоколу иметь доступ везде и ко всем сетям.

Однако, после анализа ситуации выяснялось, что к большинству виртуальных машин и/или контейнеров (типа lxc/lxd) есть доступ через какой-то интерфейс вроде (web)vnc, rest api или что-то вроде того. При этом от этого доступа отказаться совсем нельзя, потому что доступа к хостам вообще тогда не будет.

Собственно тогда, с помощью подобных плагинов можно продвинуть внедрение ansible там, где его не пускают из-за ssh. Не всегда, но ещё один довод в пользу.


Матчасть

Как ansible находит плагины?

Хотя это описано в документации, однако, в некоторых случаях в ней можно запутаться. Самый главный принцип - это разобраться, где должны лежать сами плагины в системе, чтобы ansible их нашёл. Принцип расположения плагинов следующий.

Если вам необходимо использовать плагин в нескольких проектах и с разными версиями, то есть две директории:

  • ~/.ansible/plugins - сможет использовать тот пользователь, который запускает ansible-команду. В большинстве случаев - это расположение самое правильное, потому что мы редко используем несколько пользователей для запуска. Если же так, то - это способ отделить возможности текущего пользователя от возможностей всех остальных.

  • /usr/share/ansible/plugins - в этой директории будут плагины, которые смогут найти все пользователи. Сюда нужно класть плагины с большой осторожностью, потому что эта директория самая базовая для всех версий и пользователей. Насколько я помню, она имеет меньший приоритет, но это может вызвать некоторые проблемы.

  • директория_ansible/plugins - это место расположения стандартных модулей, которые поставляются вместе с пакетом. На их примерах можно легко научиться писать собственные реализации.

А вот для конкретного запуска или версии расположить модули уже сложнее. В отдельно стоящем проекте это можно указать в ansible.cfg. Но, чаще всего используется переменные окружения вида ANSIBLE_plugin_type_PLUGINS, где plugin_type - это имя вида плагина. Собственно, список типов плагинов у ansible довольно большой:

  • action_plugins - плагин для подготовки выполнения модуля на хосте.

  • cache_plugins - кеширование/хранение фактов о хосте.

  • callback_plugins - отвечают за то, как вы увидите результаты выполнения (но не только).

  • connection_plugins - способ подключения к целевым хостам.

  • filter_plugins - дополнительные обработчики переменных в шаблонах.

  • inventory_plugins - способ обработки файла инвентаря.

  • lookup_plugins - дополнительный способ получения данных помимо фактов.

  • shell_plugins - позволяют реализовывать особенности выполнения команд на целевой машине (обёртки над оболочками).

  • strategy_plugins - определяют то, в каком порядке будут выполняться модули на хостах.

  • become_plugins - определяют способ подъёма привилегий на целевом хосте.

  • vars_plugins - способ, как наполнять переменными инвентарь из дополнительных источников (отдельная и очень интересная тема).

Это основной набор плагинов, который применим практически к любой системе. С помощью грамотно написанного набора плагинов в сочетании с модулями, можно решить практически любую задачу по автоматизации управления инфраструктурой.

Это моё личное мнение, но, я предпочитаю создавать проект в git, где все плагины будут лежать согласно директориям plugins, а ansible заставляю смотреть текущую директорию с помощью переменных. После, этот git-проект можно клонировать в директорию ~/.ansible/plugins и поддерживать в актуальном состоянии.

Как устроен плагин connection?

Основные задачи плагинов подключения в ansible:

  • Выполнить команду сформированную модулем на целевом хосте. При этом его не беспокоит, что это за команда. Ему важно именно выполнить её там, где требуется с теми правами, что требуются. Если это ssh, то нам нужно подключиться к хосту и выполнить на нём требуемую команду. Если это контейнер, то нужно приатачиться к нему и выполнить в нём нужную команду. Если мы подключаемся по vnc, то нужно настроить подключение, достичь оболочки и выполнить команду, наконец. По большому счёту можно сказать, что это способ доставки команд от модулей на хост. Все команды, в том числе служебные, выполняются на хосте именно с помощью этого плагина.

  • Отправить файл на целевой хост. Когда мы выполняем тот или иной модуль, то очень часто копируем целый скрипт на хост. По большому счёту, выполнение модуля это (зачастую) формирование python-файла, который копируется на целевую машину и выполняется на ней с помощью этого же плагина. Копирование обычно производится в какую-то временную директорию, указанную в настройках. Это и есть "ахиллесова пята" производительности в ansible, потому что довольно много трафика гоняется для довольно примитивных действий. Помимо служебного трафика, нужно так же перемещать сгенерированные шаблоны конфигов на хост и класть их по назначению.

  • Загружать с целевого хоста файлы. Мы не только загружаем файлы, но иногда нам нужно получить результат выполнения, чтобы как-то его обработать и применить. Например, мы выполняем установку какого-то кластерного решения, получаем сертификат, которые впоследствии нужно положить на другие хосты. Таким образом, мы должны прежде чем разложить файлы по хостам, сперва получить их на управляющий хост (откуда выполняется ansible-команда).

Если первые две задачи работают исключительно в тандеме, то загрузка файлов с хоста держится особнячком и выполняется конкретными модулями.

Помимо этих задач, плагин отвечает за открытие и поддержку соединения с тем API, с помощью которого производится обмен файлами и выполняются команды. Если это ssh-соединение, например, нам необходимо инициировать подключение, настроить его и передавать через него данные. Если мы реализуем плагин, который с помощью какого-то API будет выполнять команды на виртуальных машинах, то нам необходимо каким-то образом проверить возможность подключения и передачи данных к этому API, произвести авторизацию и т.п. Помимо инициализации соединения, так же необходимо корректно его завершить.

Но, в общем виде, не смотря на такое обилие задач и особенностей, плагин считается работоспособным, если имеет реализацию трёх основных задач, описанных в начале. Чтобы определить, готов ли плагин к использованию, достаточно вызвать ansible-doc -t connection -l и найти в списке непосредственно ваш. Если его нет в списке, то на это может быть три причины:

  1. Вы положили свой плагин в неверное место или не определили с помощью настроек ансибла директорию с вашими плагинами.

  2. Плагин не имеет реализации трёх абстрактных методов для выполнения базовых задач.

  3. Вы неверно определили имя транспорта подключения или оно каким-то образом пересекается с другими.

Что за имя транспорта?

Имя плагина определяется именем файла и теми параметрами которые в нём. Конкретно connection-плагин должен быть в модуле, который содержит в себе класс Connection, а в этом классе должен быть атрибут transport, который и является именем плагина соответствующего имени модуля.

Немножко мудрено, согласен, но в целом на атрибут transport зачастую можно довольно конкретно забить. Ни разу не сталкивался с тем, чтобы этот атрибут на что-то реально влиял. Впрочем, всегда стараюсь его задавать по имени модуля.

Думаю, на этом теории достаточно. Если какие-то моменты были недостаточно описаны, чтобы понять, то напишите в комментариях, и я дополню статью разъяснениями.

Практика

Инструментарий для работы

Итак, чтобы написать плагин у нас есть следующие инстурменты:

  • Абстрактный класс ansible.plugins.connection.ConnectionBase, в котором нам нужно определить необходимые для работы методы.

  • Абстрактный метод exec_command, которые принимает в себя аргументы для выполнения команды.

  • Абстрактный метод put_file, который должен реализовывать базовую логику отправки файла.

  • Абстрактный метод fetch_file, который должен реализовывать логику получения файла с хоста.

  • Атрибут объекта _play_context, который содержит необходимые параметры для подключения.

  • и многие другие вспомогательные атрибуты и тулзы.

Помимо всего прочего, чтобы получить некий аналог логгера, можно инициировать объект класса ansible.utils.display.Display, который имеет методы v по уровню детализации отображения (v для -v, vv для -vv и т.п. до vvvv). Этот метод очень полезен, чтобы записывать отладочную информацию в stdout для определения проблем с выполнением с конкретным хостом.

Ещё два полезных метода мы уже разберём в практической части статьи.

Пишем простой аналог 'local'

Нафига ещё один local?

На самом деле изначально я планировал сделать Proxmox плагин. Но когда я его написал со всеми упрощениями, получилась лютая портянка, которая была снесла крышу тем, кто начинает писать плагины. Лучше начинать с чего-то простого.

Даже в этом примере я постарался упростить код для наглядности.

Итак, собственно начнём с кода:

import getpass
import shutil
import subprocess

from ansible.plugins.connection import ConnectionBase
from ansible.module_utils._text import to_text
from ansible.utils.display import Display
from ansible.utils.path import unfrackpath


display = Display()


class Connection(ConnectionBase):
    transport = 'my'
    has_pipelining = True
    default_user = getpass.getuser()

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.cwd = None

    def _connect(self):
        if not self._connected:
            display.vvv("LOCAL CONNECTED TO {0}".format(self._play_context.remote_addr), host=self._play_context.remote_addr)
            self._connected = True

    def exec_command(self, cmd, in_data=None, sudoable=True):
        super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)

        display.vvv("LOCAL EXEC {0}".format(to_text(cmd)), host=self._play_context.remote_addr)
        if in_data:
            display.vvv("IN DATA: {}".format(in_data))

        result = subprocess.run(
            cmd,
            shell=True,
            cwd='/tmp/',
            stderr=subprocess.PIPE,
            stdout=subprocess.PIPE,
            input=in_data if in_data else None
        )

        return result.returncode, result.stdout, result.stderr

    def _copy_file(self, in_path, out_path):
        in_path = unfrackpath(in_path, basedir=self.cwd)
        out_path = unfrackpath(out_path, basedir=self.cwd)

        display.vvv("COPY FILE FROM {0} TO {1}".format(in_path, out_path), host=self._play_context.remote_addr)
        shutil.copyfile(in_path, out_path)

    def put_file(self, in_path, out_path):
        super(Connection, self).put_file(in_path, out_path)
        display.vvv(u"LOCAL PUT {0} TO {1}".format(in_path, out_path), host=self._play_context.remote_addr)
        self._copy_file(in_path, out_path)

    def fetch_file(self, in_path, out_path):
        super(Connection, self).fetch_file(in_path, out_path)

        display.vvv(u"LOCAL FETCH {0} TO {1}".format(in_path, out_path), host=self._play_context.remote_addr)
        self._copy_file(in_path, out_path)

    def close(self):
        self._connected = False

Что мы здесь имеем?

_connect()

Метод, который почему-то сделан protected (в понятиях Python). Этот метод вызывается для инициализации подключения к необходимому API. Вся его задача заключается в том, чтобы установить атрибут _connected в True. Этот метод вызывается декоратором @ensure_connect, который навешивается на все абстрактные методы, которые нам необходимо описать. Именно за этим мы делаем вызов super для этих методов. Технически, мы могли бы просто навесить декоратор на наши методы, и этого было бы достаточно, для вызова _connect().

Мы так же могли написать собственную реализацию подключения, но незачем, если есть стандартная.

exec_command

Реализация передачи вызова команды на целевой машине. Хочу напомнить, что этот метод зачастую вызывается для служебных задач, т.е. выполнения питон-скрипта на целевой машине.

Метод принимает в себя следующие аргументы:

  • cmd - строка команды, которую ansible хочет выполнить на хосте.

  • in_data - данные для stdin выполняемой команды. Некоторые команды могут объединяться в так называемый pipeline, по аналогии с | в командной строке, когда stdout одной команды передаётся в stdin другой.

  • sudoable - признак того, что команда выполняется с повышением привилегий (от sudo, очень грубо говоря).

Чтобы не перегружать код, я убрал реализацию become из плагина, но задача состоит в том, чтобы с помощью плагина become повысить привилегии команды. Доступ к плагину осуществляется с помощью атрибута become, нашего объекта подключения. В нашем плагине подключения главное - это определить, что команда запросила повышение привилегий, и что наш плагин к этому готов.

На выходе, метод должен вернуть список из трёх элементов:

  • Системный код, который вернула команда после выполнения.

  • Стандартный вывод команды.

  • Вывод ошибок команды.

Именно это потом попадает в результат выполнения модуля.

put_file

Метод, который необходим ansible для выполнения модулей на целевой машине. С помощью него, модуль загружает свой код на хост, чтобы потом выполниться с помощью ansible_python_interpreter. Т.е. порядок исполнения модуля такой:

  1. Копируем текст модуля на целевой хост с помощью put_file().

  2. С помощью shell-плагина формируем команду для того, чтобы сделать наш код исполняемым.

  3. Исполняем эту команду с помощью exec_command()

  4. С помощью shell-плагина сформировать команду на выполнение модуля.

  5. Выполнить эту команду с помощью exec_command()

  6. Удалить исходный код, сформированный модулем с хоста (команда тоже shell-плагином делается и исполняется exec_command()).

Метод достаточно простой, принимает всего два аргумента, ничего не возвращает и считается исполненным, если не свалился с ошибкой. К слову, ошибки лучше оборачивать в ansible.errors.AnsibleError и его наследников.

  • in_path - путь к файлу в системе, на которой запущен ansible.

  • out_path - путь на целевом хосте, куда этот файл нужно положить.

fetch_file

Метод, который используется модулями для получения файлов с целевого хоста. Я не буду повторяться, потому что его реализация идентична put_file(), за тем исключением, что значения аргументов в нём в обратном значении:

  • in_path - путь на целевом хосте, откуда нужно взять файл.

  • out_path - путь в системе, на которой запущен ansible, куда нужно положить файл.

close

Этот метод полезен тем, что выполнятся после того, как все команды завершены, и необходимо корректно завершить соединение, а также выполнить какие-то завершающие действия с подключением. Если мы создавали временный токен, то его можно деактивировать. Если мы создали сессию, то её можно уничтожить.

Лайфхак

Вам очень поможет разок выполнить ansible-playbook или ansible с параметром -vvvv и переменной окружения ANSIBLE_DEBUG=1. Там есть что почитать. Если вы умеете в чтение логов, то проведя маленькое исследование можно много полезной информации собрать, в том числе о том, как можно улучшить работу ансибла для своих задач.

Если вы подключили модуль, который я привёл в своей статье, то попробуйте выполнить:

# ansible all -i 127.0.0.2, -m shell -a 'uname -a' --connection my -vvv

Внимательно прочитав вывод, вы сможете почерпнуть для себя в 100500 раз больше полезного, чем от данной или любой другой статьи про ансибл.


Выводы

...каждый сделает, конечно же, сам. На практике очень удобно писать свои плагины для каких-то нестандартных решений. Так, очень удобно иметь плагин, которому не нужно будет открывать ssh на виртуальную машину, или иметь плагин, который будет подключаться к хранилищу данных инфраструктуры и формировать необходимый инвентарь, или может нужно сделать вывод информации более понятным и привлекательным, дублируя информацию в OpenSearch/ElasticSearch для дальнейшего анализа.

Написать свой плагин можно имея довольно базовые познания в программировании. Плагины имеют достаточно низкоуровневую логику, поэтому для отладки хватит выполнить любой простой модуль. Плагины переиспользуемы, поэтому любыми полезными наработками можно делиться с другими, уменьшая сложность поддержки различных систем.

В целом, ansible очень хорошо расширяется различными плагинами и модулями, за что он так любим сообществом. Надеюсь, статья была полезна для вдохновения на автоматизацию рутины, улучшения каких-то процессов или более глубокого понимания работы Ansible.

Всем бобра и вдохновения!

Комментарии (5)


  1. aborouhin
    00.00.0000 00:00
    +1

    А тот самый плагин для Proxmox, который изначально планировался в качестве примера, случайно, не дописали до конца? И если дописали - можете выложить? Я правильно понимаю, что там API, которое дёргает guest agent, используется? Полезная штука в некоторых сценариях была бы.


    1. onegreyonewhite Автор
      00.00.0000 00:00
      +1

      Нет, хотя написать его не проблема. Могу выложить в gist, когда руки дойдут, и приложить к статье.

      А в чём проблема теперь самостоятельно написать и выложить?) Самое сложно там учесть вызовы для windows машин (проблемы с кодировкой). А так, поднимаете http пул в _connect, закрываете в close, отправляете запросы в api. Файлы лучше кодировать сразу в base64.


      1. aborouhin
        00.00.0000 00:00

        Спасибо. Специально дописывать не надо, конечно :) Просто подумал, что оно Вам для своих нужд нужно было, и можно переиспользовать уже готовый велосипед.

        А так и сам напишу, конечно. Просто я на питоне последний раз что-то длиннее 10 строк писал несколько лет назад (ну не люблю я его...), и придётся в процессе в мануал лезть вспоминать синтаксис и стандартную библиотеку на каждой строке (хотя в эпоху CoPilot и ChatGPT уже, наверное, и нет...)


        1. onegreyonewhite Автор
          00.00.0000 00:00

          Можете плагин на rust+maturin написать :)

          У меня нужды прям не было, потому что для своих нужд пока ssh достаточно. Для одного заказчика писал для их особенной самописной системы виртуализации на вебсокете - было весело. Самое классное, что для написания плагина каких-то особенных знаний прям не нужно.

          А так и сам напишу, конечно.

          Поделитесь потом с сообществом. Но сразу предупреждаю, что адекватно решить проблему become не получится, потому что нет необходимых инструментов в pve api.


          1. aborouhin
            00.00.0000 00:00
            +3

            Ну мне тоже не срочно, поставил в планы на светлое будущее :) Там ещё столько всего более актуального надо в Ansible перетащить, это у меня такой долгострой...


            P.S. А ChatGPT, кстати, развеселила. Из первого варианта написанного ей плагина:

            def exec_command(self, *args, **kwargs):
                raise NotImplementedError("exec_command is not implemented for Proxmox connection plugin")
            
            def put_file(self, *args, **kwargs):
                raise NotImplementedError("put_file is not implemented for Proxmox connection plugin")
            
            def fetch_file(self, *args, **kwargs):
                raise NotImplementedError("fetch_file is not implemented for Proxmox connection plugin")

            Будучи осуждена за такую халтуру, со второй попытки выдала что-то выглядящее отдалённо работоспособным. По крайней мере, структуру плагина всю без доп. подсказок написала корректно. Заодно от неё узнал, что есть обёртка вокруг проксмоксовского API для питона - proxmoxer. Вот для таких задач оно и полезно, чтобы быстро во что-то совсем для тебя новое погрузиться... Но, конечно, надо перепроверять и доделывать.