Для жаждующих знаний и прогресса собрали материал из урока Дениса Наумова, спикера курсов Ansible и Python для инженеров. Немного разберёмся с теорией и посмотрим как написать модуль для создания пользователей в базе данных.
Материал объёмный. Рекомендуем сразу открыть итоговый код файла clickhouse.py для удобной работы со статьей.
Сначала разберемся немножко с теорией: что за такие модули для Ansible и что в Ansible есть ещё расширяемого, кроме модулей, чтобы не путаться в том, что мы можем написать для Ansible.
У нас есть модули и плагины и это не равнозначные понятия. Модуль – это то, что исполняется на удаленном хосте, то есть том хосте, который мы конфигурируем. А плагин – это то, что исполняется там, где мы вызываем наш playbook, наши роли и так далее. Плагины служат для расширения функциональности самого Ansible, интерпретации наших ролей, playbook-ов и так далее. А модуль служит для конфигурации какого-то ресурса на удаленной машине. В этом и заключаются их различия, если говорить совсем верхнеуровнево.
А далее у нас стоит такой вопрос: когда писать свой модуль?
Во-первых, если модуля с похожей функциональностью нет. Но здесь тоже следует ограничивать себя рамками разумного. Обладая модулем на bash, в котором есть такие утилиты как c URL и возможность выполнить какой-нибудь бинарник и знаниями о написании этого бинарника, можно сделать всё. Но мы ведь хотим, чтобы всё было удобно, и наша инфраструктура была описана как код. Так что если у вас нет какого-то модуля с похожей функциональностью, который уже умеет конфигурировать тот ресурс, который вы хотите сконфигурировать, например, какую-нибудь базу данных и так далее, то это хороший вариант написать свой модуль.
Далее, если pull request-а модуля с похожей функциональностью нет. Это практически то же самое, что и первый пункт, но значит, что модуль с похожей функциональностью ещё не в релизе и можно его, как минимум, скачать в виде исходного кода и использовать, а можно дождаться, когда он выйдет с ближайшим релизом Ansible.
В-третьих, если то, что вы хотите написать – не должно выполняться плагином. То есть вы не хотите дополнить то, что у вас выполняется на хосте, с которого вы запускаете свои playbook-и и роли. То есть какие-то фичи по дополнению того, как интерпретируются и выполняются вашим playbook-и и роли.
Далее, ссли то, что вы хотите написать – не должно выполняться ролью. То есть модуль служит для того, чтобы удобно описать действия над каким-либо ресурсом, а не какую-то конкретику, которую вы хотите сконфигурировать на удаленном хосте.
Ну и в конце концов, если то, что вы не хотите выполнить – должно выполняться несколькими модулями, то есть составлять какие-то SilverBullet модули не нужно. Если есть требование сконфигурировать несколько зависимых ресурсов, то, скорее всего, здесь нужно написать несколько модулей, которые умеют взаимодействовать друг с другом через последовательность, через ввод и вывод.
Разберемся в том, как взаимодействуют модули с Ansible. Взаимодействуют они просто: на удаленном хосте выполняется какой-то модуль, на вход к нему приходит какой-то json, собственно, из того action plugin-а, который вызывает наш модуль. И модуль отдает этому action plugin-у, самому Ansible, тоже какой-то json. И если немножко отдалиться, то с высоты птичьего полёта схема взаимодействия будет примерно следующей.
У нас есть хост-контроллер, с которого запускается наш Ansible. В нём есть какой-то action plugin, и он отправляет запрос на исполнение модуля на каком-то удаленном хосте. Там модуль исполняется в среде, которую создает Ansible, и возвращает в какой-то json, который action plugin также интерпретирует и выводит к нам на экран уже всё, что было сделано на нашем удаленном хосте.
Теория на этом заканчивается, давайте приступать к написанию своего модуля для Ansible. Модуль у нас будет простой, да и на самом деле, писать эти модули очень просто. Достаточно лишь знать совсем немного о программировании на Python. Всё остальное Ansible как framework – в себе предоставляет, и писать модули очень удобно.
Модуль будет заключаться в том, что есть база данных, как clickhouse, и мы хотим создавать в ней пользователей. Создавать или удалять.
Первым делом стоит вопрос: а с чего нужно начать? И в этом Ansible нам тоже помогает. У нас есть такая веб-страничка Developing Ansible module, и там мы можем увидеть, что нам нужно, чтобы подготовиться к разработке модуля на Ansible.
Для начала нужно обновить наши пакеты и установить некоторые зависимости – это некоторые библиотеки, которые служат для того, чтобы у нас удобно было разрабатывать, и предоставлялась вся функциональность, например, python-dev, libssl-dev и так далее. В общем-то, некоторые вспомогательные функции для разработки – они служат для того, чтобы мы могли запускать Ansible при разработке своего модуля или запускать в целом Python. Здесь есть инструкция для установки для различных операционных систем, например, Debian-based, CentOS-based и так далее.
Далее нам нужно создать какую-то среду окружения для того, чтобы мы могли разрабатывать.
И сначала нам говорят, что нужно спланировать repository Ansible-а. Давайте перейдем в среду разработки и склонируем repository Ansible-а. Я использую PyCharm. И в нём есть такая замечательная кнопочка «Скачать системы управления версиями». Вставляем сюда этот URL,
Клонируем. Дальше нам нужно будет перейти в скачанную директорию, и если вы не используете среду разработки, то вам нужно будет выполнить 2, 3, 4 и 5 шаги. Если вы используете тот же самый PyCharm – он сделает всё за вас.
Теперь нам нужно будет создать виртуальную среду для того, чтоб мы могли устанавливать туда свои зависимости. И у нас не возникало никакого dependency hell на уровне основного интерпретатора, а всё выполнялось в виртуальной среде, чтобы одни модули не мешали другим. Возможно, вы используете разные версии каких-то модулей и так далее. Для этого и служит виртуальная среда. Далее нужно виртуальную среду активировать, установить все зависимости и выполнить какой-то скрипт, который подготовит для нас среду (6 пункт).
Смотрим, что у нас произошло и видим, что не получилось установить Python SDK. Написано, что SDK у нас кажется не валидным.
Давайте попробуем его настроить. Это частая ошибка и у вас такая ошибка тоже может вполне возникнуть, поэтому установим. Здесь у нас написано, что должна быть какая-то виртуальная среда, но не получилось эту среду создать, поэтому давайте мы создадим новую среду. Скажем, что мы хотим создать новую среду, всё верно, мы будем использовать интерпретатор Python 3.9. Нажимаем «OK».
У нас идет создание виртуальной среды, виртуальная среда была создана. Проект индексируется, и теперь нам нужно, установить зависимости. Консоль должна подхватить ту среду, которая была создана. Для этого выполняем команду pip3 install -r requirements.txt
– это те requirements, те зависимости, который нам предоставляются вместе с репозиторием Ansible.
Давайте посмотрим, что там есть. Шаблонизатор jinja2, который используется в Ansible. PyYAML, потому что у нас конфигурации Ansible пишутся на языке программирования. На языке YAML, а как я уже сказал ранее – у нас обмен между модулем и action plugin-ом происходит в формате json, поэтому нужно каким-то образом эти YAML-ы парсить.
Остальное всё в таком же духе. Криптография, всё, что касается SSL, модуль для управления пакетами и различные другие зависимости. Как мы видим, всё у нас было установлено – это значит, что мы можем выполнить ту самую команду, которую нам предлагал выполнить Ansible.
Вот она – наша команда (6 пункт). $ . hacking/env-setup
Выполняем и видим, что у нас всё было установлено, успешно. Здесь написано «Done !». И нам даже напоминают о том, что мы должны указывать какой-то host file при помощи ключика –i.
Итак, мы уже, кажется, готовы к разработке. И давайте посмотрим, что нам нужно сделать, чтобы начать разрабатывать.
Ansible перед запуском может забирать какие-то факты о хосте. Если вам эти факты нужны, например, какие-то уже созданные базы данных в моём случае или какие-то пользователи, то можно описать по этому шаблону можуль, который будет у нас собирать факты.
Но нам здесь факты собирать не нужно – мы всё обработаем своим модулем. Кроме того, что собирать факты, можем собирать некоторую информацию. Чем информация отличается от фактов в терминологии Ansible? Факты – это то, что присуще тому хосту, на котором вы собираетесь выполнить какие-то действия, которые вы собираетесь конфигурировать, а информация – она может не относиться к этому хосту. Например, информация о доступности каких-то сервисов перед тем, как что-нибудь сконфигурировать на нашем хосте – вы, допустим, хотите сходить куда-нибудь в AWS и там выполнить какие-то действия, тоже его предварительно подготовить к работе, например, к тесному взаимодействию с вашим хостом, какой-нибудь s3 и так далее. И это уже не относится к хосту, это относится к какому-то стороннему ресурсу. Именно для этого служит этот модуль info.
Факты – это то, что относимся к нашему хосту, на котором мы что-то хотим делать или к тем ресурсам, которые конкретно на этом хосте расположены. Но, а дальше у нас есть сам модуль, который выполняет какие-то действия. И здесь написано, что мы должны перейти в директории lib/ansible/modules/ и создать там какой-то свой тестовый модуль. Но у нас модуль будет не тестовый, наш модуль будет весьма конкретным.
Тем не менее, этот пример мы скопируем.
Перейдем в среду разработки и перейдем в ту самую директорию. Директория у нас была lib library root/ansible/modules/ и здесь мы должны создать какой-то свой модуль. Он у нас будет файлом с расширением .py, поэтому мы выбираем new по этому файлу и назовем его clickhouse. Создали, я не хочу добавлять его в Git, поскольку я его не буду отправлять в виде pull request-а в основной repository Ansible. Я просто буду использовать его локально.
Здесь можно заполнить какие-то данные о том, кто создал этот модуль, но его создал явно не Terry Jones. Его создал такой человек Denis Naumov, точнее ещё не создал, а только собирается. Но и здесь какая-то почта нужнно моя <d.naumov@slurm.io>.
Очень рекомендуется это всё заполнять, указать свою лицензию и крайне рекомендуется все эти переменные тоже заполнять, поскольку потом Ansible-м, как framework-ом по ним будет построена некоторая документация. И опять же, потому что Ansible является framework-ом, в частности, тогда, когда мы создаем модули, то у нас здесь наблюдается некоторая инверсия контроля. Мы пишем какие-то действия в виде функций, мы заполняем какие-то переменные в виде значений, а Ansible как framework – уже сам потом решает, кому, когда и где их нужно применить. В этом и заключается инверсия контроля, то есть здесь мы не описываем весь ход выполнения программы, начиная с точки входа. Мы просто описываем некоторые действия, а Ansible сам потом решит, когда эти действия применять. В частности, эти действия будут применены, например, при генерации документации и при вызове нашего модуля.
Здесь мы должны назвать наш модуль, назовём его clickhouse, здесь какое-то короткое описание: This is clickhouse users management module. И здесь, опять же, у нас есть какие-то подсказки, что мы можем сделать версионирование по технологии семантик, у нас будет первая версия. И здесь какое-то длинное описание.
Я его заполнять не буду, поскольку это займет очень много времени, но если вы разрабатываете свои модули, то это описание очень рекомендуется заполнять, поскольку по нему потом можно выгрузить документацию. Да и тем, кто взаимодействует с вашим модулем, будет вполне понятно, что он делает по этому описанию. И здесь это оставим как есть. Можно ещё описать, что вообще наш модуль делает, какие-то другие значения. Но для того, чтобы код не раздувать, я его удалю.
Здесь, опять же, можно указать автора, практически все буквы даже совпадают, и здесь возможна ссылка на наш GitHub. Все эти поля вам нужно будет заполнить.
Далее идут какие-то примеры, которые позволяют понять, как с нашим модулем можно взаимодействовать. Примеры давайте уже заполним, мы сможем определиться с тем, что наш модуль будет делать. И скажем например, что у нас будет такой пример, как создание пользователя. Name, скажем, у нас будет Connect to DBMS clickhouse and create user.
Далее у нас должны быть перечислены какие-то поля. Какие поля у нас могут быть перечислены? Во-первых, это будет module clickhouse – здесь всё заполняется в формате YAML, чтоб можно было сразу скопировать куда-нибудь в playbook и выполнить. И скажем, что у нас здесь будет login_user, у нас здесь будет login_password – эти значения будут служить для того, чтобы мы могли выполнить, собственно, действия по созданию какого-то пользователя. То есть, здесь нам нужно указать данные для входа под супер-пользователем или, по крайней мере, под пользователем, который имеет гранты на то, чтобы создавать новых пользователей. И здесь у нас будет user, который будет создаваться. New_username, скажем, и пароль «password». New user’s password.
И примерно то же самое мы вместо всего этого опишем для случая, когда мы будем удалять пользователя. Нужны данные для входа под супер-пользователем – здесь нам уже пароль не нужен. И у нас будет классическое для Ansible-а состояние – absent, когда нам нужно удалить какого-то пользователя.
Здесь мы написали всё, что у нас может быть. И теперь мы должны заполнить, что у нас будет возвращаться, когда наш модуль отработает. У нас будет возвращаться какая-то структура, назовём её mutations. У неё будет какое-то описание, и это описание будет гласить, что это у нас будет заключаться в том, что мы здесь будем возвращать лист мутирующих запросов. То есть список тех запросов, которые либо удаляли, либо добавляли нового пользователя.
Далее мы должны заполнить, когда оно возвращается. Опять же, это всё нужно для документации, он у нас будет всегда возвращаться этот список, даже если он будет пустым. Далее мы заполняем тип – у нас это будет список. Далее мы должны заполнить пример того, что у нас может быть возвращено. Скажем, что у нас в примере будет какой-нибудь (Create). Конечно же, это всё будет в кавычках. Будем возвращать в таком формате: ('CREATE USER %(new_user)s {"new_user": "john"}'
), и здесь у нас будет ещё передаваться какая-то информация о том, что он у нас за new_user был. New_user, и у него будет какой-нибудь имя, например «john». И далее мы описываем версию, в которой этот модуль был добавлен – это нужно для того, если вы собираетесь пушить в As Code в vansible, например, скажем, что версии 2.8.
Далее у нас есть какая-то структура, расскажу о том, что здесь происходит. В примере здесь сделано всё, на мой взгляд, не очень аккуратно, например, это проксирующая функция – она здесь совершенно ни к чему, поэтому мы от неё избавимся.
И скажем, что главная функция у нас будет выполняться здесь. Мы описываем точку входа в наш module – это будет функция main, собственно ту, которому мы будем описывать. Здесь есть также некоторые комментарии к тому, что у нас происходит.
И первым делом мы должны заполнить список того, что наш модуль может принимать в виде аргументов, тот самый список аргументов, который мы здесь и заполнили не так давно. Давайте его скопируем и сюда вставим в виде комментария, чтобы нам было удобно заполнять. Эта строчка нам уже не нужна, здесь мы это закомментируем и перенесем. И на самом деле здесь всё, почему-то, делается через вызовы классов dict, но как вы знаете, в Python, конечно же, принято словари, по крайней мере, в Python 3, описывать в виде литералов через фигурные скобки, поскольку это просто эффективнее. А оптимизация на пустом месте – она никогда ещё лишней не была. Поэтому давайте заменим все эти объявления на нормальные. У нас теперь должны быть ключи и значения. Они у нас разделяются при помощи двоеточия, это пока не нужно. И осталось только описать поле «required».
Теперь объясняю, что это значит. Здесь мы должны описать какие-то переменные, которые будем писать в своих playbook-ах. Это будет «login_user», и здесь мы говорим, что тип будет строка. Указываем в виде строки, а не типа. Если бы мы типизировали при помощи Python, мы бы указали так, но здесь мы вынуждены написать это в виде строки. Внутри Ansible выполнить некоторую интроспекцию этого типа и проверить по нему, но здесь в конфигурации мы должны указывать это в виде строки. А, например, со значениями логического булева типа, мы можем указывать их, как есть – в виде логического типа. И поле required говорим о том, что этот аргумент должен быть обязательным. Если вы сталкивались с таким модулем, как errParse, вот участники курса Python для инженеров с ним сталкивались, здесь то же самое.
Теперь у нас должен быть логин password. Что ещё можем сделать? Можем передать имя пользователя, которое будет обязательным и являться строкой. Это всё нам уже не нужно. У нас ещё может быть пароль, а пароль уже может быть не обязательным. Как мы знаем, пароль пользователя, которого мы создаём, у нас не обязателен, когда мы будем этого пользователя удалять. Ещё у нас есть какое-то состояния «state», если мы его указываем как Absent, то у нас пользователь будет удаляться. И мы тоже скажем, что оно не нужно и здесь воспользуемся ещё одним вариантом ключика. И ещё один вариант ключика у нас заключается в том, что можем написать сюда не что иное, как, например, «default». И здесь указываем какое-то значение по умолчанию. Если ключик не был передан, то есть «state» не Absent, то по умолчанию будет state «new», мы создаем нового пользователя. Уберем все training commas, все запятые, которые нам не нужны.
И давайте двигаться к описанию нашего модуля. Что должно быть дальше?
Здесь у нас есть какой-то результат, который возвращается по умолчанию и описан через вызов класса, давайте переделаем. Добавим немножко оптимизации. Все вызовы присваивания изменим с «равно» на «двоеточие». Есть «origina_message», «message» – нам всё это не нужно. И такой у нас результат по умолчанию будет, вcе эти строки, которые служат для помощи – мы тоже уберем.
И далее мы объявляем класс AnsibleModule и его объект. Он у нас импортирован из того, что нам предоставляет Ansible – это некоторые helper-ы Ansible-а, как framework для написания модуля. И здесь мы говорим, что у нашего модуля будут такие аргументы через именованный argument – argument_spec. И говорим, что наш модуль поддерживает check_mode.
Обработаем check_mode. Если у нас check_mode, то мы просто выходим с каким-то результатом. Чтобы вернуть из модуля наш json, используется такая функция класса AnsibleModule, как exit_json. Есть и другие функции, с ними мы тоже сегодня познакомимся, но, по крайней мере, с той функцией, которая позволяет нам выйти с ошибкой. Чтобы наш action plugin понял, что у нас playbook завершился неуспешно, модуль не выполнился на хосте и вернул какую-то ошибку, которую мы сами и пишем.
В дальнейшем идет работа нашим модулем. Здесь мы что-то делаем, собственно, с нашим результатом, есть какие-то значения. Возвращается какой-то результат, так что на этом скелет нашего модуля готов. Давайте приступать к его реализации.
Во-первых, нам понадобится внешняя библиотека, внешний модуль. И есть некоторая неудобность Ansible-а: по той идеологии, по которой он построен – мы не можем устанавливать какие-то модули, не оповестив при этом пользователя, то есть не получим от пользователя при этом команду. Под пользователем здесь понимается тот, кто пишет роли и playbook-и. То есть здесь мы модуль просто так установить не можем. Нужно сделать так, чтобы тот, кто запускает playbook, увидел, что такого модуля нет, и предпринял какие-либо действия. То есть просто так незаметно мы ничего на хостовую машину установить не можем, в этом и заключается идеология Ansible. Но что же делать, если нам нужна какая-то внешняя библиотека? Как её установить? Конечно же, устанавливать мы её будем в том же playbook-е через пакетные менеджеры, а с импортом немножко всё будет обстоять сложнее, поскольку нам нужно как-то оповестить того, кто наш playbook будет запускать.
И давайте приступим к реализации этого самого оповещения. Нам нужен будет такой модуль, как clickhouse-driver. Если у вас он не установлен, вы можете его установить при помощи менеджера pip3 install clickhouse-driver
. И теперь из этого модуля я должен кое-что импортировать. From clickhouse_driver я должен импортировать тот класс, который будет позволять мне отправлять запросы к базе данных, которая будет находиться на удаленном хосте. И этот класс называется Client. Но чтобы не писать просто Client, не понятно, что за клиент, я назову его CHClient. Потому что я импортировал из этого модуля некий alias, и он будет называться CHClient. Он у меня по этой переменной будет доступен, и здесь я её определю как None.
И теперь я должен этот модуль импортировать. Если он там не будет установлен, в Python будет сгенерировано исключение, которое носит такое имя, как ImportError. Я должен поймать этот ImportError и какие-то действия совершить. Я мог бы, наверное, сделать вот так и если у меня произошёл ImportError, то здесь я установлю значение CHClient равным None.
Но на самом деле можно сделать чуть удобнее и короче. Как вы знаете, Python – это про удобность, понятность и лаконичность, поэтому я воспользуюсь модулем contextlib, которая предоставляет нам некоторые контекстные менеджеры и импортирую оттуда такой контекстный менеджер, как suppress. И теперь я могу написать вот так: with suppress, поскольку это контекстный менеджер. Здесь я указываю исключения, которые я хочу подавить. И здесь просто вызываю эту строку, а здесь я присваиваю None к нашей CHClient, в ту переменную, в которую что-нибудь должно быть импортировано.
Таким образом, исключение у меня будет подавлено, и никаких пустых вызовов в except и сам except я писать при этом не должен.
Осталось проверить: есть ли у нас что-то в этой переменной CHClient. Давайте проверим это перед тем, как мы будем проверять, что у нас в модуле check_mode. Мы скажем, что, если у нас CHClient равен None. На None мы проверяем через is, поскольку экземпляр None создается один на всю программу при запуске интерпретатора, то его выгоднее проверять по ссылке, а не по значению. И если он у нас является None, то есть если эти ссылки совпадают, то я должен сделать следующее.
Я должен оповестить того, кто запускает playbook о том, что что-то пошло не так. Такого модуля нет. И для этого я из модуля, из этой функции верну то, что у меня может быть сгенерировано при помощи функции, при помощи метода, класса AnsibleModule, который носит название fail_json. Таким образом, в тот json, который мы сюда передадим, будут добавлены некоторые технические параметры, и так action plugin сможет понять, что что-то пошло не так, и наш модуль не был выполнен. И сюда мы передаем просто строку, в которой напишем: clickhouse-driver module is required. И таким образом по сообщению понятно, что нам нужно установить на хосте этот модуль. Эту ситуацию мы обработали.
Давайте сделаем это до check_mode-а ,чтобы сразу было понятно, что у нас до check_mode-а должна быть возвращена, в случае чего, какая-то ошибка. Потому что так у нас check_mode, вроде, будет успешным, если объявить эту внизу, но, тем не менее, когда мы решим использовать это уже = в боевом режиме, то у нас возникнет какая-то ошибка, что не очень хорошо. А здесь мы можем увидеть эту ошибку и подготовиться.
Далее сделаем следующее действие: мы научим наш модуль создавать пользователя. Для этого нам нужно распарсить те аргументы, которые мы принимаем из модуля. Давайте их распарсим, просто получим их значение. Создадим переменную login_user и скажем, что переменная login_user у нас будет равна вот такому вызову. Мы можем обратиться к входным аргументам нашего модуля через вызов члена класса module под названием params, он будет представлять собой словарь, поэтому мы как к словарю, к нему можем обращаться при помощи get, например, или просто при помощи оператора «квадратные скобки», и в таком случае мы получим значение. Этот аргумент является обязательным, и поэтому можем смело обращаться через квадратные скобки, не опасаясь того, что такое значение не будет передано, поскольку здесь мы объявили «required»: True. И даже до то того, как у нас эта часть кода выполнится, наш модуль оповестит о том, что у нас какие-то аргументы не были введены.
Попробуем создать подключение к базе данных. Сделаем это в блоке try, поскольку у нас может подключение не состояться. Назовем переменную ch_client и присвоим в ней объект класса client из модуля ckickhouse-driver. Здесь должны указать host, я сделаю это в виде именованного argument-а. И у нас всё будет выполняться на «localhost», поскольку мы на удаленной машине какие-то действия выполняем.
Теперь нужно передать пользователю, под которым будет произведен вход в базу данных clickhouse и пароль, с которым этот пользователь будет пытаться войти. Это такие аргументы, как user и password. Здесь у нас login_password. Здесь попытались войти и теперь нам нужно перехватить какие-то исключения. И сделаю страшную штуку – я перехвачу все возможные исключения.
Объясню, почему я так сделал, хотя так делать и не очень-то хорошо. Здесь мне нужно перехватить любое исключение, которое только может быть сгенерировано, а exception – это такой общий класс для всех исключений. И если что-то пошло не так, не важно, в какой строке, такого аргумента нет. Какая-то сетевая ошибка произошла, какой-то порт закрыт и так далее, и даже вплоть до того, что здесь я какой-то оператор не так применил, сделал опечатку, не закрыл скобку и так далее – всё это в этой строке я должен увидеть, поскольку без подключения к базе данных у меня ничего не получится. И если мы пишем какой-то код, где нам не нужно ловить любое исключение, где нужно поймать конкретное, обработать конкретную ситуацию, например, выход за границы списка или какую-нибудь ещё.
Допустим, то, что у нас в словаре нет такого ключа – здесь должны были перехватывать конкретные исключения, поскольку если мы хотим проверить, что у нас есть тот или иной ключ в словаре, например, и делаем это при помощи перехвата исключений или метода get, а у нас синтаксис сломается, мы обработаем совершенно не ту ситуацию, и это может выйти нам боком, иногда бывает тут же полностью уронить программу, чем придать ей не консистентное состояние, и это повлияет на наш бизнес, собственно, логику которого и реализует наша программа.
Немножко отвлеклись. Я объяснил, почему я здесь перехватываю совершенно любое исключение, и если оно у меня возникло, то я сделаю следующее: снова верну какой-то fail_json. Сделаем так и воспользуемся некоторым helper-ом, который предоставляет Ansible как framework и для этого нам кое-что нужно будет импортировать.
Нам нужно импортировать следующий helper. Этот helper у нас будет называться module_utils. Мне нужна такая директория Ansible module_utils_text и оттуда я должен импортировать эту функцию to_native. Но кажется, у нас такого модуля нет. Давайте разбираться с тем, куда потерялся module, он должен быть в module_utils.
Давайте перейдем в module_utils, всё у нас под боком. И здесь у нас должен быть text, и, кажется, что он у нас даже есть. И всё дело в том, что у меня не настроена среда разработки для того, чтобы я мог видеть. Давайте скажем, что у меня будет скрипт pass располагаться в ansible/lib/ansible/modules и мой модуль clickhouse. И скажем, что у меня working directory будет в самом корне этого проекта.
Там функция to_native, которая позволяет нам из какого-то объекта сделать понятный текст, и я из этого объекта сделаю понятный текст. Я передал тот объект ошибки, которые мы поймали в функцию to_native и по ней будет построена какая-то строка.
Продолжим реализацию нашей логики. Мы должны понять, что у нас происходит с пользователем, например, добавление пользователя или удаление пользователя. И для этого мы скажем, что должен быть следующий argument. Давайте даже назовем его не user_state, а state, и получим из нашего модуля через параметры то, что у нас находится в переменный под названием state, в аргументе под названием state. Как мы помним, по умолчанию здесь у нас будет new. А если мы введем что-то своё, то у нас здесь будет что-то свое. И далее мы проверим, если ли у нас state равен, например, new, то есть делаем какое-то действие. В противном случае, если у нас state равен absent, то мы тоже выполним какое-то действие. Мы позовём функцию create_new_user. А здесь мы позовем функцию delete_user. Ну а в ином случае мы сделаем ни что иное, как опять-таки вернем fail_json и скажем, что state вот такой. У нас не поддерживается этим модулем.
Приступим к реализации наших функций. Они нам должны вернуть какой-то результат. Этот результат мы должны будем вернуть в наш action plugin. Как мы видим, здесь у нас результат должен быть в виде словаря передан.
Этот словарь сюда потом распакуется. В этом словаре у нас должен быть такой параметр, как changed и какие-то свои аргументы, которые мы хотим передать. Например, это могут быть такие аргументы, как ровно те, что мы описывали, request и так далее. В общем все то, что вы захотите.
У нас будет функция create_new_user. Давайте сразу подумаем о том, какие аргументы она должна будет принять. Во-первых, она должна будет принять Client. Во-вторых, она должна принять имя пользователя, которого мы создаём. Давайте его получим здесь, поскольку будет использоваться в нескольких местах. Мы будем получать user. И здесь она будет называться user. А вот пароль нам нужен только в одном случае. Сюда мы нашего user-а передадим и пароль. А пароль мы получим через модуль. Вроде бы все, что нужно передали. В delete_user мы будем передавать примерно тоже самое, за исключением пароля. Он там нам совершенно не нужен.
Итак, нам нужна функция create_new_user, давайте объявим ее повыше. Объявим ее при помощи ключевого слова def. В Python так функции объявляются. И здесь у нас уже будет не вызов, а какой-то аргумент. Правильно их называть параметрами. Аргументы — это то, что мы передаём при вызове функции, параметры — это то, что у нас описывается внутри функции.
И здесь мы уже должны проверить, а есть ли у нас такой пользователь. И для этого нам будет нужна еще одна функция, как ни странно. Назовем ее is_user_exist. Пользуемся тем, что мы можем называть функции, начиная со слова with, и тогда сразу понятно, что они вернут какое-то значение логического типа. И давайте вот с этой функции реализацию и начнем. Эта функция должна сходить в clickhouse и спросить: «Есть ли у нас такой user?». Передали сюда Client. И у нашего клиента мы можем вызвать функцию execute. Это деталь реализации clickhouse-driver-а. Я здесь не буду на них останавливаться, скажу только, что вот эта вот функция вернет нам список каких-то кортежей, которые собой строки символизируют. То есть это вся наша выборка якобы. У нас первая строка.Какие-то значения, =в зависимости от того, что мы запросили. Вторая строка, третья и так далее. Вот в таком формате у нас будут строки возвращены.
Напишем execute запрос. Я напишу его и прокомментирую, что он делает. Он убирает количество пользователей из таблицы system.users. Эта та табличка, в которой хранятся пользователи. Системная табличка, в которой хранятся пользователи clickhouse. Выбрали количество пользователей из system.users. И где у нас name должен быть равен некоторому параметру. Мы могли бы сделать это форматированной строкой. И сюда захардкодить, но таким образом у нас появится SQL-инъекция, что не очень хорошо. Поэтому мы воспользуемся экранированием. Библиотека позволяет нам какие-то значения экранировать. Для того, чтобы экранировать, мы должны вот такую вот запись объявить, что у нас будет какой-то аргумент и он будет являться строкой. И здесь скажем, что этот аргумент будет называться user. И вторым параметром мы можем передать словарик, в котором мы говорим, что у нас есть user. Вот именно то, как мы назвали здесь наш аргумент, также должен называться ключ. Значением у него будет тот user, который мы передали в параметрах функций. У нас здесь вернется список строк. Нам важно только первое значение оттуда, и выбираем мы только одно значение. Выберем самую первую строку, ноль. Потому что в Python индексация с нуля. И выберем самое первое значение, опять-таки ноль, потому что индексация с нуля. И сравним его с нулем. То есть у нас здесь будет True, если здесь значение больше нуля, то есть пользователь существует. И false, если у нас значение не больше нуля, то есть пользователя не существует. Вернем сразу это логическое значение, почему бы нет, и наша функция успешно реализована.
Теперь здесь должны мы проверить о том, что у нас пользователь существует. Но для начала пишем, что у нас будет какой-то список запросов выполненных, мутирующих. Этот запрос он у нас не был мутирующим, поэтому его мы можем не записывать. Но здесь нам список понадобится на тот случай, если мы пользователя все-таки создадим. И скажем, что если у нас пользователь такой существует, то мы выполним следующий код. Мы вернем здесь changed false. И вернем здесь пустой список этих самых запросов. То есть вернем то, что у нас в переменной queries, а там сейчас у нас пустой список. Если же у нас такого пользователя не существует, то мы должны его создать. Для этого пишем запрос, которым будет делать. Запрос тоже достаточно простенький CREATE_USER. Сlickhouse поддерживает создание пользователя через SQL. Правда для этого нужно в конфигурационном файле подключить кое-какую опцию. USER IDENTIFIED BY какой-то пароль.
Итак, создали наш запрос. Давайте опишем еще некоторые параметры, с которыми всё будет выполнено. USER у нас будет равен USER. И password у нас будет равен password. И далее нам осталось выполнить запрос через Client. Query, query_params, здесь мы что-то выполнили, пользователь у нас был создан, никаких нам данных возвращать отсюда не нужно. И здесь мы можем теперь дополнить наш список при помощи значения, что у нас был выполнен запрос какой-то с такими-то параметрами. Вроде бы все также, как мы описывали в документации. И теперь мы должны вернуть значение. Давайте отсюда скопируем. И здесь у нас изменится только то, что changed у нас будет True, поскольку мы все-таки выполнили мутирующую операцию на каком-то целевом хосте.
Опишем функцию, которую пользователь у нас удаляет. Мы уже практически в самом конце пути реализации нашего модуля. Создаем нашу функцию. И делать она будет примерно все тоже самое. Для того, чтобы было быстрее, мы скопируем. Только здесь скажем, что если у нас такого пользователя не существует, то есть удалять нечего, то мы вернем changed false. И вернем запросы какие-то.
Далее нам нужно удалить пользователя. Если такой пользователь все-таки есть, и здесь нам пароль его не нужен, нам нужна просто команда drop user. Опять же здесь нам пароль не нужен, удаляем, и здесь все будет оставаться абсолютно также. На самом деле, можно написать какую-нибудь супер-функцию, которая будет все вот эти вот части выполнять как бы за скобкой.
А мы должны будем описать только вот эти вот части логики.
Например, для этого хорошо подойдет декоратор, но мы таким заморачиваться не будем, это отдельная тема. И, фактически, на этом моменте наш модуль является дописанным.
Проверим, что мы вернули какой-то результат. В конце выйдем из нашего модуля с каким-то json-ом, который у нас строится из этого результата, здесь return писать не нужно, поскольку это за нас сделает функция exit_json.
По модулю у нас все.
Давайте приступать к тому, как мы можем сконфигурировать хост для проверки. Для того, чтобы мы могли сконфигурировать хост для проверки нам нужен какой-то контейнер с ClickHouse. Давайте запустим его в Docker, почему бы нет. Вот эту команду я, честно, скопировал из Docker Hub Яндекса, где у них лежит, собственно, образ с ClickHouse.
docker run -d --name clickhouse-host --ulimit nofile=262144:262144 yandex/clickhouse-server
Запускаем. Контейнер был запущен, давайте в этот контейнер зайдем, у меня для этого используется alias gobash gobash clickhouse-host
, но делает он, конечно же, docker exec -it
, имя контейнера bash. Сюда мы зашли. И так как у нас ClickHouse подключается по ssh, нам нужно установить сюда openssh server.
apt update && apt install -y openssh-server vim
Я заодно vim установлю, чтобы я мог какие-то конфигурационные файлы удобно редактировать. Все устанавливается. После этого мы должны будем root сменить пароль.
Ждем пока все установится. Все было установлено. Меняем пароль для root passwd root
- q1w2e3
, повторяем, пароль был успешно обновлен, и теперь мне нужно переконфигурировать конфигурации ClickHouse и openssh server. Давайте начнем с openssh server.
vi /etc/ssh/sshd_config
sshd config, порт у нас будет 22, будем слушать адрес, ipv6 мне не нужен. Конечно же, будем мы аутентифицироваться при помощи пароля, никаких ключей я сюда прокидывать не буду.
Сохраняем, перезагрузим службу ssh /etc/init.d/ssh restart
и отредактируем теконфигурационные файлы, которые у нас нужны для ClickHouse. vi /etc/clickhouse-server/users.xml
Все дело в том, что нам нужно разрешать нашему дефолтному пользователю создавать при помощи sql или удалять пользователей. По умолчанию этого делать нельзя. Можно это делать только через вот эти xml файлы, но для того, чтобы можно было это делать через xml мне нужна вот эта вот опция, ее мы раскомментируем.
Сохраняем и есть здесь одна загвоздка. Все дело в том, что ClickHouse автоматически конфигурацию не перечитывает. Для того, чтоб он ее перечитал нужно перезапустить, но так как у нас в Docker контейнер запустился, при помощи Entry Point, как раз-таки с запуском самого clickHouse-server, то у нас контейнер упадет. Ну, нам нужно будет его после этого всего лишь переподнять и скажем, что etc/init.d/clickhouse-server restart
Контейнер наш благополучно свалился, его нет docker ps
, но вот здесь он в запущенных docker ps -a
, поэтому мы скажем docker start clickhouse-host
и теперь он у нас должен быть запущен. Все верно, он у нас запущен. Перейдем в bash gobash clickhouse-host
и посмотрим, что сейчас у нас все в порядке. cat etc/clickhouse-server/users.xml
. Идем в описание нашего дефолтного user-а. И видим здесь, что у нас теперь есть возможность управлять при помощи xml.
Это значит, что мы можем, при помощи ssh, к контейнеру подключиться. Для этого давайте вызовем docker inspect clickhouse-host
и, есть у него вот такой IPAddress.
Давайте к нему подключимся и, в данный момент, почему-то подключения у нас не получается. Давайте разбираться почему. Connection refused. Зайдем на наш сервер, перейдем в конфигурацию ssh, видим, что здесь мы можем подключаться. Давайте раскомментируем вот это значение AddressFamily any
, PermitRootLogin Yes
, PasswordAuthentification Yes
. На всякий случай раскомментируем все, что у нас касается HostKey...
, PubKeyAuthentication yes
и так далее. Кажется, что на этом-то все должно быть.
Попробуем подключиться и, в этот раз, у нас все уже хорошо ssh root@172.17.0.2
. Но я к такому хосту уже когда-то подключался, возможно, что-то тестировал, поэтому мне нужно обновить список доверенных хостов.
Да. Ввожу пароль и вот я на нашем clickhouse-server, давайте сюда подключимся уже при помощи clickhouse client. Для этого выполним команду docker exec -it название контейнера и здесь мы воспользуемся тем, что у нас называется clickhouse client. docker exec -it clickhouse-host clickhouse-client
. Встроенный клиент. И посмотрим, что у нас за пользователи присутствуют SHOW USERS
. Или воспользуемся той командой, которую я вводил SELECT NAME FROM SYSTEM.USERS
. И видим, что у нас есть пользователь default.
Итак, мы сконфигурировали наш стенд и, на самом деле, именно так и будут тестироваться модули и у вас в том числе. Вы можете сделать unit-тесты и сказать, что после выполнения такой-то функции у вас должно быть такое-то состояние и так далее. То есть протестировать, так скажем, структуру вашего модуля, но тем не менее, чтобы понять, что оно на реальных системах все делается правильно, то вам придется делать интеграционные тесты. А это значит, что вам придется поднимать какое-нибудь окружение. Есть framework-и, которые упрощают тестирование в том плане, что они предоставляют какие-нибудь абстракции для проверки тех или иных значений. То есть посмотреть, что было сгенерировано после выполнения playbook, сохранить это в переменную, и с чем-нибудь сравнить, при помощи того же paytest и так далее. Но суть заключается в том, что все будет происходить на какой-то тестовой среде, в тех же самых контейнерах, например, или на каком-нибудь кластере Kubernetes-а тоже в каких-нибудь контейнерах и так далее.
Сделаем следующее – нам нужно создать, во-первых, какой-нибудь инвентарь. Создаем инвентарь, в Git я его добавлять не хочу и скажу, что здесь у нас будет такая группа CH-servers. И в ней будет мой хост, который я видел. Запустим еще одну консоль и выполним быстро docker inspect
. С таким адресом и далее я скажу, что у меня будут какие-то данные для подключения. В тестовых целях, конечно же, рефакторить я ничего не буду. Ansible_connection, далее будет ansible_user, это будет root и будет ansible_ssh_pass. Кажется, он пишется вот так, у меня это вот такой пароль. Инвентарь мы описали.
И теперь нам нужно описать, собственно, наш playbook.
Для этого вернемся на страничку с написанием модуль для ansible. И вот здесь написано, как можно протестировать код, который мы написали для модуля в playbook. И здесь, вот, какой-то playbook, который якобы предопределен. Назовем этот PlayBook, как нам говорит seo ansible testmod. Сделаем testmod.yaml.
Нам нужно сделать следующее. Test my new module, назовём Test clickhouse module, будем выполнять всё, на всех хостах, и скажем, что здесь, теперь, я хочу выполнить свой модуль.
Для этого, мне нужно описать что мой модуль будет делать. Воспользуюсь тем, что у меня есть какое-то описание run new module. О'кей, выполняем. Здесь мне нужны отступы. При помощи зажатия колесика, я, кстати, могу выделять в PyCharm несколько строк, если кто-то не знает, может быть весьма полезно. Логин user у меня будет default, пароль у меня будет, по умолчанию, пустой, с таким паролем создаёт clickhouse. И здесь мы скажем, что у нас будет создаваться пользователь New_User, и пароль у него будет new_user. И здесь модуль выведет то, что у нас было получено в виде ответа.
Давайте попробуем выполнить. Для этого, мы находимся в терминале. Что у нас в этом терминале? В этом терминале у нас clickhouse, как мы видим никаких пользователей у нас ещё нет. И попробуем выполнить наш модуль. Опять же, если у кого-то вызывает затруднение, здесь написано, как выполнить этот playbook.
Нет такого модуля Ansible написано, где у меня здесь написано ansible, testmod.yml ansible playBook from ansible import context. Написано, что модуль не найден. Кажется, что здесь у меня какие-то проблемы, опять-таки, сказываются с настройкой окружения. Давайте попробуем их починить.
Здесь у меня есть какой-то venv, виртуальная среда, Python. Почему же такого модуля у меня нет. Наверное, нужно выполнить ещё, какую-нибудь, конфигурирующую команду. И да, действительно, вот она. Если бы я всё это читал, я бы знал о том, что эту команду нужно выполнить . hacking/env-setup
. Какую-то среду я себе создал. Пробую выполнить ansible-playbook ./testmod.yaml
. Написано, что нет таких хостов. Но, все дело в том, что мне нужно указать какой-то инвентарь, в котором находится inventory.ini ansible-playbook -i inventory.ini ./testmod.yaml
. Пробуем выполнить, ansible начал собирать какие-то факты и написано, действительно, что clickhouse driver module у нас требуется на нашем хосте.
Кстати, ту ситуацию, с тем, когда у нас запрещено было бы создавать пользователей через SQL-интерфейс, можно тоже в этом модуле обработать. И оповещать, что, вот, у нас нельзя создавать пользователей в нашем clickhouse через ускоренный интерфейс. Переконфигурируйте сам clickhouse. Ну, мы видим, что вот эту ошибку, мы описывали как раз-таки здесь, и вот она наша ошибка высветилась, наш Action Plugin понял, что что-то пошло не так и зафейлил то, что у нас здесь есть.
Теперь нам нужно установить окружение. Для того, чтобы установить окружение, нам нужно перед этим выполнить ещё две задачи. Во-первых, нам нужно установить на наш хост pip 3, это мы будем делать при помощи такого модуля, как apt, поскольку там Debian используется. Это я знаю, что там Debian используется, у вас это, конечно же, могут быть различные операционные системы, и вы через шабанизацию должны определить, что за операционная система там стоит.
Буду устанавливать Python 3 pip, а после того, как я установил pip, я буду устанавливать, вот этот самый clickhouse-driver. Install clickhouse-driver, который у меня почему-то не скопировался, теперь скопировался, всё в порядке, и здесь мне нужен модуль pip. И здесь я буду устанавливать, как раз-таки, clickhouse Driver. Кажется, вот так, у меня всё должно работать.
Весь итоговый код файла testmod.yaml
Ansible пошёл собирать факты, устанавливает pip3, это может занять какое-то время. Мы подождём пока он устанавливает, и пока опишем, ещё какую-нибудь, задачу по удалению пользователя, например. Здесь у нас будет run new module, мы скажем, что create clickhouse user, и здесь он будет удалять clickhouse user. Здесь нам пароль больше не нужен, а вот state нам нужен правильный. State у нас будет absent и, конечно, же здесь мы тоже должны что-нибудь вывести. Ну, и как мы видим, у нас playbook отработал теперь уже успешно, были выполнены команды Create User IDENTIFIED password.
Был создан user, new User password и написано, что модуль у нас не устанавливает, no_log. Давайте установим и посмотрим, что у нас там с пользователями. New_user, теперь давайте вот это пока что закомментируем, и попробуем выполнить playbook ещё раз. У нас он должен выполняться идемпотентно, то есть никаких изменений, у нас не должно быть произведено. И действительно, видим, что у нас всё выполнилось идемпотентно. То есть у нас никаких изменений не произведено changed false, никаких запросов выполнено не было.
Теперь давайте это закомментируем. Допустим, нам нужно удалить нашего пользователя, а вот это, мы, как раз-таки, уже раскомментируем.
Удаляем пользователя state absent, тот же самый пользователь у нас будет удалён. И видим, что у нас была выполнена команда DROP User User New_User. Давайте перейдём в эту консоль, и посмотрим, что у нас пользователь New_User исчез. Было:
Действительно, исчез.
Проверим идемпотентность. Идемпотентность работает, changed false. Попробуем передать какой-нибудь state, который у нас non_absent. Написано, что у нас FAILED, output должен быть скрыт, потому что у нас стоит no_log: true. Давайте закомментируем. State non_absent не поддерживается в этом модуле.
Всё работает, именно так, как мы того и хотели. Про написание модулей для Ansible это всё. Весь итоговый код файла clickhouse.py.