Пишем свои модули для Ansible на Python

image-loader.svg

Для жаждующих знаний и прогресса собрали материал из урока Дениса Наумова, спикера курсов Ansible и Python для инженеров. Немного разберёмся с теорией и посмотрим как написать модуль для создания пользователей в базе данных.

Материал объёмный. Рекомендуем сразу открыть итоговый код файла clickhouse.py для удобной работы со статьей.

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

image-loader.svg

У нас есть модули и плагины и это не равнозначные понятия. Модуль — это то, что исполняется на удаленном хосте, то есть том хосте, который мы конфигурируем. А плагин — это то, что исполняется там, где мы вызываем наш playbook, наши роли и так далее. Плагины служат для расширения функциональности самого Ansible, интерпретации наших ролей, playbook-ов и так далее. А модуль служит для конфигурации какого-то ресурса на удаленной машине. В этом и заключаются их различия, если говорить совсем верхнеуровнево.

А далее у нас стоит такой вопрос: когда писать свой модуль?

image-loader.svg

Во-первых, если модуля с похожей функциональностью нет. Но здесь тоже следует ограничивать себя рамками разумного. Обладая модулем на bash, в котором есть такие утилиты как c URL и возможность выполнить какой-нибудь бинарник и знаниями о написании этого бинарника, можно сделать всё. Но мы ведь хотим, чтобы всё было удобно, и наша инфраструктура была описана как код. Так что если у вас нет какого-то модуля с похожей функциональностью, который уже умеет конфигурировать тот ресурс, который вы хотите сконфигурировать, например, какую-нибудь базу данных и так далее, то это хороший вариант написать свой модуль.

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

В-третьих, если то, что вы хотите написать — не должно выполняться плагином. То есть вы не хотите дополнить то, что у вас выполняется на хосте, с которого вы запускаете свои playbook-и и роли. То есть какие-то фичи по дополнению того, как интерпретируются и выполняются вашим playbook-и и роли.

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

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

image-loader.svg

Разберемся в том, как взаимодействуют модули с 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.

image-loader.svg

Для начала нужно обновить наши пакеты и установить некоторые зависимости — это некоторые библиотеки, которые служат для того, чтобы у нас удобно было разрабатывать, и предоставлялась вся функциональность, например, python-dev, libssl-dev и так далее. В общем-то, некоторые вспомогательные функции для разработки — они служат для того, чтобы мы могли запускать Ansible при разработке своего модуля или запускать в целом Python. Здесь есть инструкция для установки для различных операционных систем, например, Debian-based, CentOS-based и так далее.

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

image-loader.svg

И сначала нам говорят, что нужно спланировать repository Ansible-а. Давайте перейдем в среду разработки и склонируем repository Ansible-а. Я использую PyCharm. И в нём есть такая замечательная кнопочка «Скачать системы управления версиями». Вставляем сюда этот URL,

image-loader.svg

Клонируем. Дальше нам нужно будет перейти в скачанную директорию, и если вы не используете среду разработки, то вам нужно будет выполнить 2, 3, 4 и 5 шаги. Если вы используете тот же самый PyCharm — он сделает всё за вас.

Теперь нам нужно будет создать виртуальную среду для того, чтоб мы могли устанавливать туда свои зависимости. И у нас не возникало никакого dependency hell на уровне основного интерпретатора, а всё выполнялось в виртуальной среде, чтобы одни модули не мешали другим. Возможно, вы используете разные версии каких-то модулей и так далее. Для этого и служит виртуальная среда. Далее нужно виртуальную среду активировать, установить все зависимости и выполнить какой-то скрипт, который подготовит для нас среду (6 пункт).

image-loader.svg

Смотрим, что у нас произошло и видим, что не получилось установить Python SDK. Написано, что SDK у нас кажется не валидным.

image-loader.svg

Давайте попробуем его настроить. Это частая ошибка и у вас такая ошибка тоже может вполне возникнуть, поэтому установим. Здесь у нас написано, что должна быть какая-то виртуальная среда, но не получилось эту среду создать, поэтому давайте мы создадим новую среду. Скажем, что мы хотим создать новую среду, всё верно, мы будем использовать интерпретатор Python 3.9. Нажимаем «OK».

image-loader.svg

У нас идет создание виртуальной среды, виртуальная среда была создана. Проект индексируется, и теперь нам нужно, установить зависимости. Консоль должна подхватить ту среду, которая была создана. Для этого выполняем команду pip3 install -r requirements.txt — это те requirements, те зависимости, который нам предоставляются вместе с репозиторием Ansible.

Давайте посмотрим, что там есть. Шаблонизатор jinja2, который используется в Ansible. PyYAML, потому что у нас конфигурации Ansible пишутся на языке программирования. На языке YAML, а как я уже сказал ранее — у нас обмен между модулем и action plugin-ом происходит в формате json, поэтому нужно каким-то образом эти YAML-ы парсить.

image-loader.svg

Остальное всё в таком же духе. Криптография, всё, что касается SSL, модуль для управления пакетами и различные другие зависимости. Как мы видим, всё у нас было установлено — это значит, что мы можем выполнить ту самую команду, которую нам предлагал выполнить Ansible.

Вот она — наша команда (6 пункт). $ . hacking/env-setup

image-loader.svg

Выполняем и видим, что у нас всё было установлено, успешно. Здесь написано «Done!». И нам даже напоминают о том, что мы должны указывать какой-то host file при помощи ключика –i.

image-loader.svg

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

Ansible перед запуском может забирать какие-то факты о хосте. Если вам эти факты нужны, например, какие-то уже созданные базы данных в моём случае или какие-то пользователи, то можно описать по этому шаблону можуль, который будет у нас собирать факты.

image-loader.svg

Но нам здесь факты собирать не нужно — мы всё обработаем своим модулем. Кроме того, что собирать факты, можем собирать некоторую информацию. Чем информация отличается от фактов в терминологии Ansible? Факты — это то, что присуще тому хосту, на котором вы собираетесь выполнить какие-то действия, которые вы собираетесь конфигурировать, а информация — она может не относиться к этому хосту. Например, информация о доступности каких-то сервисов перед тем, как что-нибудь сконфигурировать на нашем хосте — вы, допустим, хотите сходить куда-нибудь в AWS и там выполнить какие-то действия, тоже его предварительно подготовить к работе, например, к тесному взаимодействию с вашим хостом, какой-нибудь s3 и так далее. И это уже не относится к хосту, это относится к какому-то стороннему ресурсу. Именно для этого служит этот модуль info.

Факты — это то, что относимся к нашему хосту, на котором мы что-то хотим делать или к тем ресурсам, которые конкретно на этом хосте расположены. Но, а дальше у нас есть сам модуль, который выполняет какие-то действия. И здесь написано, что мы должны перейти в директории lib/ansible/modules/ и создать там какой-то свой тестовый модуль. Но у нас модуль будет не тестовый, наш модуль будет весьма конкретным.

Тем не менее, этот пример мы скопируем.

image-loader.svg

Перейдем в среду разработки и перейдем в ту самую директорию. Директория у нас была lib library root/ansible/modules/ и здесь мы должны создать какой-то свой модуль. Он у нас будет файлом с расширением .py, поэтому мы выбираем new по этому файлу и назовем его clickhouse. Создали, я не хочу добавлять его в Git, поскольку я его не буду отправлять в виде pull request-а в основной repository Ansible. Я просто буду использовать его локально.

image-loader.svg

Здесь можно заполнить какие-то данные о том, кто создал этот модуль, но его создал явно не Terry Jones. Его создал такой человек Denis Naumov, точнее ещё не создал, а только собирается. Но и здесь какая-то почта нужнно моя .

Очень рекомендуется это всё заполнять, указать свою лицензию и крайне рекомендуется все эти переменные тоже заполнять, поскольку потом Ansible-м, как framework-ом по ним будет построена некоторая документация. И опять же, потому что Ansible является framework-ом, в частности, тогда, когда мы создаем модули, то у нас здесь наблюдается некоторая инверсия контроля. Мы пишем какие-то действия в виде функций, мы заполняем какие-то переменные в виде значений, а Ansible как framework — уже сам потом решает, кому, когда и где их нужно применить. В этом и заключается инверсия контроля, то есть здесь мы не описываем весь ход выполнения программы, начиная с точки входа. Мы просто описываем некоторые действия, а Ansible сам потом решит, когда эти действия применять. В частности, эти действия будут применены, например, при генерации документации и при вызове нашего модуля.

Здесь мы должны назвать наш модуль, назовём его clickhouse, здесь какое-то короткое описание: This is clickhouse users management module. И здесь, опять же, у нас есть какие-то подсказки, что мы можем сделать версионирование по технологии семантик, у нас будет первая версия. И здесь какое-то длинное описание.

image-loader.svg

Я его заполнять не буду, поскольку это займет очень много времени, но если вы разрабатываете свои модули, то это описание очень рекомендуется заполнять, поскольку по нему потом можно выгрузить документацию. Да и тем, кто взаимодействует с вашим модулем, будет вполне понятно, что он делает по этому описанию. И здесь это оставим как есть. Можно ещё описать, что вообще наш модуль делает, какие-то другие значения. Но для того, чтобы код не раздувать, я его удалю.

image-loader.svg

Здесь, опять же, можно указать автора, практически все буквы даже совпадают, и здесь возможна ссылка на наш GitHub. Все эти поля вам нужно будет заполнить.

image-loader.svg

Далее идут какие-то примеры, которые позволяют понять, как с нашим модулем можно взаимодействовать. Примеры давайте уже заполним, мы сможем определиться с тем, что наш модуль будет делать. И скажем например, что у нас будет такой пример, как создание пользователя. 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, когда нам нужно удалить какого-то пользователя.

image-loader.svg

Здесь мы написали всё, что у нас может быть. И теперь мы должны заполнить, что у нас будет возвращаться, когда наш модуль отработает. У нас будет возвращаться какая-то структура, назовём её mutations. У неё будет какое-то описание, и это описание будет гласить, что это у нас будет заключаться в том, что мы здесь будем возвращать лист мутирующих запросов. То есть список тех запросов, которые либо удаляли, либо добавляли нового пользователя.

Далее мы должны заполнить, когда оно возвращается. Опять же, это всё нужно для документации, он у нас будет всегда возвращаться этот список, даже если он будет пустым. Далее мы заполняем тип — у нас это будет список. Далее мы должны заполнить пример того, что у нас может быть возвращено. Скажем, что у нас в примере будет какой-нибудь (Create). Конечно же, это всё будет в кавычках. Будем возвращать в таком формате: ('CREATE USER %(new_user)s {"new_user": "john"}'), и здесь у нас будет ещё передаваться какая-то информация о том, что он у нас за new_user был. New_user, и у него будет какой-нибудь имя, например «john». И далее мы описываем версию, в которой этот модуль был добавлен — это нужно для того, если вы собираетесь пушить в As Code в vansible, например, скажем, что версии 2.8.

image-loader.svg

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

image-loader.svg

И скажем, что главная функция у нас будет выполняться здесь. Мы описываем точку входа в наш module — это будет функция main, собственно ту, которому мы будем описывать. Здесь есть также некоторые комментарии к тому, что у нас происходит.

image-loader.svg

И первым делом мы должны заполнить список того, что наш модуль может принимать в виде аргументов, тот самый список аргументов, который мы здесь и заполнили не так давно. Давайте его скопируем и сюда вставим в виде комментария, чтобы нам было удобно заполнять. Эта строчка нам уже не нужна, здесь мы это закомментируем и перенесем. И на самом деле здесь всё, почему-то, делается через вызовы классов dict, но как вы знаете, в Python, конечно же, принято словари, по крайней мере, в Python 3, описывать в виде литералов через фигурные скобки, поскольку это просто эффективнее. А оптимизация на пустом месте — она никогда ещё лишней не была. Поэтому давайте заменим все эти объявления на нормальные. У нас теперь должны быть ключи и значения. Они у нас разделяются при помощи двоеточия, это пока не нужно. И осталось только описать поле «required».

Теперь объясняю, что это значит. Здесь мы должны описать какие-то переменные, которые будем писать в своих playbook-ах. Это будет «login_user», и здесь мы говорим, что тип будет строка. Указываем в виде строки, а не типа. Если бы мы типизировали при помощи Python, мы бы указали так, но здесь мы вынуждены написать это в виде строки. Внутри Ansible выполнить некоторую интроспекцию этого типа и проверить по нему, но здесь в конфигурации мы должны указывать это в виде строки. А, например, со значениями логического булева типа, мы можем указывать их, как есть — в виде логического типа. И поле required говорим о том, что этот аргумент должен быть обязательным. Если вы сталкивались с таким модулем, как errParse, вот участники курса Python для инженеров с ним сталкивались, здесь то же самое.

Теперь у нас должен быть логин password. Что ещё можем сделать? Можем передать имя пользователя, которое будет обязательным и являться строкой. Это всё нам уже не нужно. У нас ещё может быть пароль, а пароль уже может быть не обязательным. Как мы знаем, пароль пользователя, которого мы создаём, у нас не обязателен, когда мы будем этого пользователя удалять. Ещё у нас есть какое-то состояния «state», если мы его указываем как Absent, то у нас пользователь будет удаляться. И мы тоже скажем, что оно не нужно и здесь воспользуемся ещё одним вариантом ключика. И ещё один вариант ключика у нас заключается в том, что можем написать сюда не что иное, как, например, «default». И здесь указываем какое-то значение по умолчанию. Если ключик не был передан, то есть «state» не Absent, то по умолчанию будет state «new», мы создаем нового пользователя. Уберем все training commas, все запятые, которые нам не нужны.

image-loader.svg

И давайте двигаться к описанию нашего модуля. Что должно быть дальше?

image-loader.svg

Здесь у нас есть какой-то результат, который возвращается по умолчанию и описан через вызов класса, давайте переделаем. Добавим немножко оптимизации. Все вызовы присваивания изменим с «равно» на «двоеточие». Есть «origina_message», «message» — нам всё это не нужно. И такой у нас результат по умолчанию будет, вcе эти строки, которые служат для помощи — мы тоже уберем.

И далее мы объявляем класс AnsibleModule и его объект. Он у нас импортирован из того, что нам предоставляет Ansible — это некоторые helper-ы Ansible-а, как framework для написания модуля. И здесь мы говорим, что у нашего модуля будут такие аргументы через именованный argument — argument_spec. И говорим, что наш модуль поддерживает check_mode.

image-loader.svg

Обработаем check_mode. Если у нас check_mode, то мы просто выходим с каким-то результатом. Чтобы вернуть из модуля наш json, используется такая функция класса AnsibleModule, как exit_json. Есть и другие функции, с ними мы тоже сегодня познакомимся, но, по крайней мере, с той функцией, которая позволяет нам выйти с ошибкой. Чтобы наш action plugin понял, что у нас playbook завершился неуспешно, модуль не выполнился на хосте и вернул какую-то ошибку, которую мы сами и пишем.

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

image-loader.svg

Во-первых, нам понадобится внешняя библиотека, внешний модуль. И есть некоторая неудобность 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.

image-loader.svg

Но на самом деле можно сделать чуть удобнее и короче. Как вы знаете, Python — это про удобность, понятность и лаконичность, поэтому я воспользуюсь модулем contextlib, которая предоставляет нам некоторые контекстные менеджеры и импортирую оттуда такой контекстный менеджер, как suppress. И теперь я могу написать вот так: with suppress, поскольку это контекстный менеджер. Здесь я указываю исключения, которые я хочу подавить. И здесь просто вызываю эту строку, а здесь я присваиваю None к нашей CHClient, в ту переменную, в которую что-нибудь должно быть импортировано.

image-loader.svg

Таким образом, исключение у меня будет подавлено, и никаких пустых вызовов в 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, вроде, будет успешным, если объявить эту внизу, но, тем не менее, когда мы решим использовать это уже = в боевом режиме, то у нас возникнет какая-то ошибка, что не очень хорошо. А здесь мы можем увидеть эту ошибку и подготовиться.

image-loader.svg

Далее сделаем следующее действие: мы научим наш модуль создавать пользователя. Для этого нам нужно распарсить те аргументы, которые мы принимаем из модуля. Давайте их распарсим, просто получим их значение. Создадим переменную login_user и скажем, что переменная login_user у нас будет равна вот такому вызову. Мы можем обратиться к входным аргументам нашего модуля через вызов члена класса module под названием params, он будет представлять собой словарь, поэтому мы как к словарю, к нему можем обращаться при помощи get, например, или просто при помощи оператора «квадратные скобки», и в таком случае мы получим значение. Этот аргумент является обязательным, и поэтому можем смело обращаться через квадратные скобки, не опасаясь того, что такое значение не будет передано, поскольку здесь мы объявили «required»: True. И даже до то того, как у нас эта часть кода выполнится, наш модуль оповестит о том, что у нас какие-то аргументы не были введены.

image-loader.svg

Попробуем создать подключение к базе данных. Сделаем это в блоке try, поскольку у нас может подключение не состояться. Назовем переменную ch_client и присвоим в ней объект класса client из модуля ckickhouse-driver. Здесь должны указать host, я сделаю это в виде именованного argument-а. И у нас всё будет выполняться на «localhost», поскольку мы на удаленной машине какие-то действия выполняем.

Теперь нужно передать пользователю, под которым будет произведен вход в базу данных clickhouse и пароль, с которым этот пользователь будет пытаться войти. Это такие аргументы, как user и password. Здесь у нас login_password. Здесь попытались войти и теперь нам нужно перехватить какие-то исключения. И сделаю страшную штуку — я перехвачу все возможные исключения.

image-loader.svg

Объясню, почему я так сделал, хотя так делать и не очень-то хорошо. Здесь мне нужно перехватить любое исключение, которое только может быть сгенерировано, а exception — это такой общий класс для всех исключений. И если что-то пошло не так, не важно, в какой строке, такого аргумента нет. Какая-то сетевая ошибка произошла, какой-то порт закрыт и так далее, и даже вплоть до того, что здесь я какой-то оператор не так применил, сделал опечатку, не закрыл скобку и так далее — всё это в этой строке я должен увидеть, поскольку без подключения к базе данных у меня ничего не получится. И если мы пишем какой-то код, где нам не нужно ловить любое исключение, где нужно поймать конкретное, обработать конкретную ситуацию, например, выход за границы списка или какую-нибудь ещё.

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

Немножко отвлеклись. Я объяснил, почему я здесь перехватываю совершенно любое исключение, и если оно у меня возникло, то я сделаю следующее: снова верну какой-то fail_json. Сделаем так и воспользуемся некоторым helper-ом, который предоставляет Ansible как framework и для этого нам кое-что нужно будет импортировать.

image-loader.svg

Нам нужно импортировать следующий helper. Этот helper у нас будет называться module_utils. Мне нужна такая директория Ansible module_utils_text и оттуда я должен импортировать эту функцию to_native. Но кажется, у нас такого модуля нет. Давайте разбираться с тем, куда потерялся module, он должен быть в module_utils.

Давайте перейдем в module_utils, всё у нас под боком. И здесь у нас должен быть text, и, кажется, что он у нас даже есть. И всё дело в том, что у меня не настроена среда разработки для того, чтобы я мог видеть. Давайте скажем, что у меня будет скрипт pass располагаться в ansible/lib/ansible/modules и мой модуль clickhouse. И скажем, что у меня working directory будет в самом корне этого проекта.

image-loader.svg

Там функция to_native, которая позволяет нам из какого-то объекта сделать понятный текст, и я из этого объекта сделаю понятный текст. Я передал тот объект ошибки, которые мы поймали в функцию to_native и по ней будет построена какая-то строка.

image-loader.svg

Продолжим реализацию нашей логики. Мы должны понять, что у нас происходит с пользователем, например, добавление пользователя или удаление пользователя. И для этого мы скажем, что должен быть следующий argument. Давайте даже назовем его не user_state, а state, и получим из нашего модуля через параметры то, что у нас находится в переменный под названием state, в аргументе под названием state. Как мы помним, по умолчанию здесь у нас будет new. А если мы введем что-то своё, то у нас здесь будет что-то свое. И далее мы проверим, если ли у нас state равен, например, new, то есть делаем какое-то действие. В противном случае, если у нас state равен absent, то мы тоже выполним какое-то действие. Мы позовём функцию create_new_user. А здесь мы позовем функцию delete_user. Ну, а в ином случае мы сделаем ни что иное, как опять-таки вернем fail_json и скажем, что state вот такой. У нас не поддерживается этим модулем.

Приступим к реализации наших функций. Они нам должны вернуть какой-то результат. Этот результат мы должны будем вернуть в наш action plugin. Как мы видим, здесь у нас результат должен быть в виде словаря передан.

image-loader.svg

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

У нас будет функция create_new_user. Давайте сразу подумаем о том, какие аргументы она должна будет принять. Во-первых, она должна будет принять Client. Во-вторых, она должна принять имя пользователя, которого мы создаём. Давайте его получим здесь, поскольку будет использоваться в нескольких местах. Мы будем получать user. И здесь она будет называться user. А вот пароль нам нужен только в одном случае. Сюда мы нашего user-а передадим и пароль. А пароль мы получим через модуль. Вроде бы все, что нужно передали. В delete_user мы будем передавать примерно тоже самое, за исключением пароля. Он там нам совершенно не нужен.

image-loader.svg

Итак, нам нужна функция create_new_user, давайте объявим ее повыше. Объявим ее при помощи ключевого слова def. В Python так функции объявляются. И здесь у нас уже будет не вызов, а какой-то аргумент. Правильно их называть параметрами. Аргументы — это то, что мы передаём при вызове функции, параметры — это то, что у нас описывается внутри функции.

И здесь мы уже должны проверить, а есть ли у нас такой пользователь. И для этого нам будет нужна еще одна функция, как ни странно. Назовем ее is_user_exist. Пользуемся тем, что мы можем называть функции, начиная со слова with, и тогда сразу понятно, что они вернут какое-то значение логического типа. И давайте вот с этой функции реализацию и начнем. Эта функция должна сходить в clickhouse и спросить: «Есть ли у нас такой user?». Передали сюда Client. И у нашего клиента мы можем вызвать функцию execute. Это деталь реализации clickhouse-driver-а. Я здесь не буду на них останавливаться, скажу только, что вот эта вот функция вернет нам список каких-то кортежей, которые собой строки символизируют. То есть это вся наша выборка якобы. У нас первая строка.Какие-то значения, =в зависимости от того, что мы запросили. Вторая строка, третья и так далее. Вот в таком формате у нас будут строки возвращены.

image-loader.svg

Напишем execute запрос. Я напишу его и прокомментирую, что он делает. Он убирает количество пользователей из таблицы system.users. Эта та табличка, в которой хранятся пользователи. Системная табличка, в которой хранятся пользователи clickhouse. Выбрали количество пользователей из system.users. И где у нас name должен быть равен некоторому параметру. Мы могли бы сделать это форматированной строкой. И сюда захардкодить, но таким образом у нас появится SQL-инъекция, что не очень хорошо. Поэтому мы воспользуемся экранированием. Библиотека позволяет нам какие-то значения экранировать. Для того, чтобы экранировать, мы должны вот такую вот запись объявить, что у нас будет какой-то аргумент и он будет являться строкой. И здесь скажем, что этот аргумент будет называться user. И вторым параметром мы можем передать словарик, в котором мы говорим, что у нас есть user. Вот именно то, как мы назвали здесь наш аргумент, также должен называться ключ. Значением у него будет тот user, который мы передали в параметрах функций. У нас здесь вернется список строк. Нам важно только первое значение оттуда, и выбираем мы только одно значение. Выбе

© Habrahabr.ru