В предыдущей статье, я рассказал, как можно упростить себе жизнь, воспользовавшись возможностями динамического инвентаря в виде скрипта. Это очень удобно, когда нужно вытянуть структуру хостов из какой-то системы со всеми нужными параметрами.
Но, как я уже тогда сказал, этот способ зашит в 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
и найти в списке непосредственно ваш. Если его нет в списке, то на это может быть три причины:
Вы положили свой плагин в неверное место или не определили с помощью настроек ансибла директорию с вашими плагинами.
Плагин не имеет реализации трёх абстрактных методов для выполнения базовых задач.
Вы неверно определили имя транспорта подключения или оно каким-то образом пересекается с другими.
Что за имя транспорта?
Имя плагина определяется именем файла и теми параметрами которые в нём. Конкретно 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
. Т.е. порядок исполнения модуля такой:
Копируем текст модуля на целевой хост с помощью
put_file()
.С помощью shell-плагина формируем команду для того, чтобы сделать наш код исполняемым.
Исполняем эту команду с помощью
exec_command()
С помощью shell-плагина сформировать команду на выполнение модуля.
Выполнить эту команду с помощью
exec_command()
Удалить исходный код, сформированный модулем с хоста (команда тоже 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.
Всем бобра и вдохновения!
aborouhin
А тот самый плагин для Proxmox, который изначально планировался в качестве примера, случайно, не дописали до конца? И если дописали - можете выложить? Я правильно понимаю, что там API, которое дёргает guest agent, используется? Полезная штука в некоторых сценариях была бы.
onegreyonewhite Автор
Нет, хотя написать его не проблема. Могу выложить в gist, когда руки дойдут, и приложить к статье.
А в чём проблема теперь самостоятельно написать и выложить?) Самое сложно там учесть вызовы для windows машин (проблемы с кодировкой). А так, поднимаете http пул в _connect, закрываете в close, отправляете запросы в api. Файлы лучше кодировать сразу в base64.
aborouhin
Спасибо. Специально дописывать не надо, конечно :) Просто подумал, что оно Вам для своих нужд нужно было, и можно переиспользовать уже готовый велосипед.
А так и сам напишу, конечно. Просто я на питоне последний раз что-то длиннее 10 строк писал несколько лет назад (ну не люблю я его...), и придётся в процессе в мануал лезть вспоминать синтаксис и стандартную библиотеку на каждой строке (хотя в эпоху CoPilot и ChatGPT уже, наверное, и нет...)
onegreyonewhite Автор
Можете плагин на rust+maturin написать :)
У меня нужды прям не было, потому что для своих нужд пока ssh достаточно. Для одного заказчика писал для их особенной самописной системы виртуализации на вебсокете - было весело. Самое классное, что для написания плагина каких-то особенных знаний прям не нужно.
Поделитесь потом с сообществом. Но сразу предупреждаю, что адекватно решить проблему become не получится, потому что нет необходимых инструментов в pve api.
aborouhin
Ну мне тоже не срочно, поставил в планы на светлое будущее :) Там ещё столько всего более актуального надо в Ansible перетащить, это у меня такой долгострой...
P.S. А ChatGPT, кстати, развеселила. Из первого варианта написанного ей плагина:
Будучи осуждена за такую халтуру, со второй попытки выдала что-то выглядящее отдалённо работоспособным. По крайней мере, структуру плагина всю без доп. подсказок написала корректно. Заодно от неё узнал, что есть обёртка вокруг проксмоксовского API для питона - proxmoxer. Вот для таких задач оно и полезно, чтобы быстро во что-то совсем для тебя новое погрузиться... Но, конечно, надо перепроверять и доделывать.