Cotea: программный контроль исполнения Ansible

Привет!

Я Давид Бадалян, работаю в Исследовательском центре доверенного искуственного интеллекта ИСП РАН. В статье я хочу поговорить об Ansible — одной из самых популярных систем по автоматизации развёртывания. 

Стоит запустить Ansible программно, и он становится черным ящиком — нет никакого контроля над его выполнением, нет информации о тасках. Эту проблему мы обнаружили, разрабатывая оркестратор Michman для сервисов уровня PaaS. В результате мы создали cotea и gocotea: инструменты для программного исполнения Ansible-плейбуков из языков Python и Go.

Про cotea, её архитектуру и кейсы применения я расскажу подробно под катом. Если вы DevOps-инженер и хотите узнать, как можно гибко использовать Ansible — статья точно для вас.

Итак, в нашем оркестраторе Michman развёртывание сервисов происходит с помощью Ansible, что предполагает его программный запуск. В этом случае Ansible предоставляет либо интерфейс командной строки, либо — для систем на языке Python — запуск через ansible-runner. Но Michman реализован на языке Go. И даже если использовать ansible-runner, управлять выполнением Ansible не выйдет. Ansible-runner только показывает произошедшие события без возможности какого-либо программного контроля.

Между тем для DevOps-инженеров часто важен детальный контроль развёртывания, который предполагает реагирование на различные события по ходу выполнения в автоматическом режиме. Программное управление Ansible позволило бы определять текущее состояние выполнения и принимать гибкие решения в зависимости от ситуации. К примеру, хочется иметь доступ к такой информации:

  • результаты выполнения тасков (tasks — составных частей сценариев Ansible);

  • сообщения об ошибках;

  • значения переменных и фактов Ansible.

Для решения этой задачи мы и разработали cotea и gocotea— инструменты для программного контроля выполнения Ansible-плейбуков (речь исключительно об CLI ansible-playbook) на Python и Go соответственно. Gocotea, по сути, является портом cotea для языка Go. Само портирование мы сделали с помощью нашего собственного инструмента gopython: это библиотека, которая позволяет встраивать произвольный Python-код в программы на Go. Для этого gopython использует пакет cgo, который позволяет вызывать код на С из программы на Go. Это даёт возможность дёргать CPython API. Gopython может помочь в портировании на Go и других Python-библиотек. 

b2b58b0e286c9ef4034312b36a2548cf.jpeg

Инструменты cotea, gocotea, gopython и Michman разработаны командой облачных технологий ИСП РАН в рамках имеющихся проектов. О gopython и gocotea будет рассказано в наших следующих статьях. А сейчас я хочу поговорить об архитектуре инструмента cotea и примерах его использования. 

Архитектура cotea

Скажу точнее, что мы имеем в виду под задачей программного контроля. В случае с плейбуками Ansible речь идёт об итерировании по плеям (plays — по сути, это список тасков) и таскам.

Главная идея в Cotea (с латыни COntrol Thread Execution Ansible) — встроить в выполнение Ansible особые обработчики, которые вызываются до или после вызовов определённых функций (методов) Ansible. Обработчики могут приостанавливать работу Ansible и использовать ссылки на внутренние объекты среды выполнения. 

Обработчики встраиваются в исходный код Ansible с помощью так называемых классов-декораторов, которые в свою очередь основаны на питоновских декораторах. Механизм классов-декораторов мы придумали при разработке cotea. Его аналогов среди  общепринятых паттернов Python не нашли. Поэтому мы надеемся, что вам такой подход может пригодиться  сам по себе — он успешно решает ряд задач по мониторингу выполнения питоновских функций. Что же это за задачи?

Классы-декораторы

Классы-декораторы позволяют не только встраивать код до и/или после вызовов определенных функций, но и сохранять ссылки на объекты, полученные из аргументов функции или её возвращаемого значения. Минимальный код класса-декоратора представлен ниже:

class decore_class:

    def __init__(self, target_func):

        self.args_to_store = None

        self.result_to_store = None

        self.target_func = target_func

    def __call__(self, args):

        # сюда можно вставить код, который будет выполняться до вызова

        self.args_to_store = args

        result = self.target_func (args) # выполняется вызов 

        #, а сюда — код, который выполнится после вызова

        self.result_to_store = result

        return result

Покажем на примере механику работы классов-декораторов. Предположим, существует некая функция some_func:

def some_func (arg):

    # some actions

    return result

Установим класс-декоратор на вызов этой функции:

decore_obj = decore_class (some_func)

some_func = decore_obj

Теперь при вызове в программе функции some_func вызовется обработчик из __call__ с нужными нам действиями до и после вызова.  И главное — будут сохранены аргументы функции и её результат в полях args_to_store и result_to_store объекта decore_obj. Этот объект останется в памяти в отличие от объектов контекста завершившейся функции. 

Теперь вернёмся к Ansible. Пусть в исходном коде у нас есть функция RunTask (Task), которая возвращает объект, описывающий результат выполнения таска. Если мы установим класс-декоратор на подобную функцию, мы будем знать, какой таск только что выполнился и с каким результатом завершился. Ровно эта конструкция и лежит в основе cotea. (Эх, был бы в исходном коде Ansible и правда метод RunTask…, но тогда разработка была бы не такой интересной)

Перехват управления

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

  • запускаем Ansible в отдельном потоке;

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

Интерфейсы cotea

Интерфейсы для выполнения Ansible и итерирования по плеям и таскам мы объединили в классе runner. Также в этом классе есть интерфейсы для совершения и других действий в runtime-e, помогающих разделять и властвовать лучше контролировать выполнение и получать информацию о нём. Для начала посмотрим на самые основные интерфейсы:

  • has_next_play () — возвращает истину, если ещё остался невыполненный плей, и продвигает выполнение Ansible до момента начала запуска тасков этого плея;

  • has_next_task () — возвращает истину, если в текущем плее остался невыполненный таск и продвигает выполнение до момента перед его запуском;

  • run_next_task () — продвигает выполнение Ansible до точки, в которой тот самый next task (о наличии которого сигнализировал has_next_task) уже выполнен, и возвращает массив с результатами его (таска) работы на каждом хосте из текущего inventory (списока хостов, на которых выполняются таски);

  • finish_ansible () — завершает выполнение Ansible.

Базовая схема запуска Ansible через cotea выглядит так:

r = runner (…)

while r.has_next_play ():

    while r.has_next_task ():

        task_results = r.run_next_task ()

r.finish_ansible ()

Что ещё есть в классе runner:

  • get_next_task ()/get_next_task_name () — возвращает Ansible-объект/имя следующего таска;

  • get_prev_task ()/get_prev_task_name () — возвращает Ansible-объект/имя предыдущего таска;

  • get_cur_play_name () — возвращает имя текущего плея;

  • get_error_msg () — возвращает сообщение об ошибке таска, неудача которого стала фатальной стала причиной преждевременного завершения плейбука (т.е. на таске не было метки ignore_errors);

  • get_variable (name) — возвращает значение Ansible-переменной или факта с указанным именем;

  • add_var_as_extra_var (new_var_name, value) — создаёт Ansible-переменную с заданным именем и значением в качестве дополнительной (extra-var) переменной (динамечески добавленная переменная будет иметь приоритеты обычных extra-vars);

  • add_next_task (new_task: str) — создаёт таск с переданным текстовым описанием и добавляет его следующим в очередь на выполнение. 

grpc-cotea

В дополнение к классу runner мы реализовали и grpc-сервер по выполнению Ansible, который использует описанные выше интерфейсы cotea. Такой сервер позволяет вынести исполнение Ansible в отдельную сущность. Нужда в этом возникла, когда мы переводили оркестратор Michman на стандарт TOSCA с микросервисной архитектурой в основе. Эта версия Michman на данный момент не является open source. 

Пока мы не выложили grpc-cotea в открытый доступ, но планируем это сделать.

Программный контроль выполнения

Теперь я расскажу, как мы применяли cotea для одной внутренней задачи. Вообще при разработке плейбуков часто нужно как-то обрабатывать результаты выполнения тасков Ansible. Например, аргументами очередного таска могут быть результаты одного из предыдущих. Довольно часто приходится извлекать необходимые аргументы из результата предыдущего таска вручную (например, с помощью register). 

При этом возникает куча проблем, связанная с обработкой строк и приведением типов — это всё нужно делать непосредственно в среде выполнения Ansible (рантайме). Можно применить модули Ansible, но часто бывает, что необходимого модуля нет — или никто не сделал, или нет модуля для нужной версии Ansible. А через сotea для такой промежуточной обработки можно применить любые Python-библиотеки, потому что мы даем программный доступ к результатам выполнения.

Наша задача заключалась в развёртывании хранилища секретов Vault. Ansible у нас был версии 2.9.4 — это одна из самых популярных. Но модулей для Podman и Vault тогда в ней не было. Какие проблемы нам нужно было решить:

  • Обработать вывод команды «podman ps…» для проверки, не запущен ли уже контейнер Vault;

  • Извлечь ключи Vault из вывода команды «vault operator init …»;

  • Обработать вывод команды «vault secrets list…» — узнать, не существует ли уже заданный секрет Vault.

Ниже я привожу код для решения этих задач с помощью cotea. Метод run_next_task возвращает список объектов TaskResult, соответствующих результатам выполнения запущенных тасков на каждом из хостов текущего inventory. TaskResult содержит такие поля, как stdout, stderr, msg. После выполнения таска, который запускал какую-нибудь команду — «podman ps» или одну из двух других, — в поле stdout будет информация, которую нужно обработать. Чтобы понимать в runtime-е, какой именно таск выполнялся, можно вызвать метод get_prev_task_name

# инициализация объекта cotea.runner 

#podman_ps_task_name = »…»

#vault_init_task_name = »…»

#vault_secrets_task_name = »…»

# r = runner (…)

while r.has_next_play ():

    while r.has_next_task ():

        task_results = r.run_next_task ()

        prev_task_name = r.get_prev_task_name ()

        if prev_task_name == podman_ps_task_name:

            task_result = task_results[0]

            cmd_stdout = task_result.result[«stdout»]

            # загружаем состояние контейнера

            cmd_stdout_json = json.loads (cmd_stdout)

            state_field = cmd_stdout_json[0][«State»]

            r.add_var_as_extra_var («VAULT_CONTAINER_STATE», state_field)

        elif prev_task_name== vault_init_task_name:

            task_result = task_results[0]

            cmd_stdout = task_result.result[«stdout»]

            # извлекаем ключи

            unseal_key = get_unseal_key (cmd_stdout)

            root_token = get_root_token (cmd_stdout)

            r.add_var_as_extra_var («VAULT_UNSEAL_KEY», unseal_key)

            r.add_var_as_extra_var («VAULT_ROOT_TOKEN», root_token)

        elif prev_task_name == vault_secrets_task_name:

            task_result = task_results[0]

            cmd_stdout = task_result.result[«stdout»]

            # извлекаем секреты

            created_secrets = get_secrets (cmd_stdout)

            michman_created = False

            if «michman/» in created_secrets:

                michman_created = True

            r.add_var_as_extra_var («MICHMAN_SECRETS_CREATED», michman_created)

r.finish_ansible ()

if r.was_error ():

    print («ansible-playbook launch — ERROR:»)

    print (r.get_error_msg ())

else:

    print («ansible-playbook launch — OK»)

В коде выше, обработка результата podman ps в листинге видна целиком — это всего две строчки. Для остальных команд обработку мы вынесли в отдельные функции. Результаты обработки добавляем Ansible runtime через дополнительные переменные окружения через метод add_var_as_extra_var из cotea.runner. Как вы помните, такие переменные будут доступны в следующих тасках.

Интерактивный режим cotea

Я уже говорил, что cotea — классный инструмент интерфейсы cotea можно положить в основу полноценного отладчика Ansible и реализовать его в виде плагина для среды разработки. Такая работа уже есть в наших планах. Исходный код доступен, и сделать свой плагин может любой желающий.

А тем временем на основе cotea мы уже реализовали возможность перехода в интерактивный режим. Для этого нужно вызвать метод interactive_discotech (вначале название было шуточным, но всем понравилось и я оставил) cotea.runner-а. Вызвать его можно, по идее, где душе угодно — действия пользователя будут менять некоторые внутренние объекты Ansible, влиять на порядок выполнения и т. д. Но наиболее целесообразно и рекомендуется вызывать его при неудаче таска. Такой способ вызова и показан в нашей документации по интерактивному режиму. В том числе, интерактивный режим cotea позволяет добавить новую Ansible-переменную или новый таск — и всё это в runtime-е. Такой инструментарий крайне полезен при работе с большими сценариями. Мы сами в шоке используем интерактивный режим cotea при развертывании OpenStack и весьма довольны жизнью. 

fbed0afaf881a90de96ac8e96143cd9d.png

Вот какой функционал мы реализовали (иду прямо по командам на рисунке):

  • вывод полной информации об упавшем таске, включая все обычные параметры: аргументы, окружение, переменные, теги (команда ft);

  • вывод всех сообщений об ошибках выполнения тасков (включая проигнорированные ошибки) в порядке возникновения (команда msg);

  • повторный запуск упавшего таска (команда re);

  • продолжение выполнения сценария: упавший таск игнорируем и выполнение продолжаем со следующего таска (команда c);

  • добавление новой Ansible-переменной как дополнительной (extra-var, команда v);

  • динамическое добавление нового таска — ввод с консоли в виде строки (команда nt);

  • просмотр значения переменной (команда w).

Для примера давайте рассмотрим такую задачу. Предположим, что имеется плейбук с большим количеством тасков, и ближе к концу в нём есть следующих два таска:

 — name: Create conf file

   file:

     path:»{{ target_conf_file_path }}»

     state: touch

     mode: '0600'

 — name: Run python script

   shell: «python {{ working_dir }}/main.py»

   register: command_output

Теперь давайте предположим, что переменной target_conf_file_path почему-то нет, а на целевых хостах не сделали ссылку /usr/bin/python на интерпретатор Python. Это тривиальные проблемы с тривиальными решениями. Но всё равно придется перезапускать плейбук, а когда для его выполнения нужно минут 30, а то и больше, это очень неудобно. Именно с подобного рода ситуациями мы нередко сталкиваемся, работая с большими Ansible-сценариями.

На скриншоте я показал, как этот инцидент разрешается с помощью интерактивного режима cotea — весь сценарий перезапускать не придётся. 

ad9d928349458bc5f93e690593a69ac2.png

Решение: с помощью команды «v» добавляем переменную target_conf_file_path с соответствующим значением, а потом командой «re» перезапускаем таск «create conf file». Как видите, она завершается успешно.

Решение: с помощью команды «nt» добавляем новый таск. Этот таск создаст правильную символическую ссылку /usr/bin/python на всех целевых хостах. Task вводится пользователем в виде строки. Пока при вводе нужно соблюсти все отступы и табуляции (как видно на рисунке выше) так же, как это делается в плейбуке. Затем используем команду «с», которая в данном случае (как и написано в подсказке пользователю) выполнит сначала новый таск, а затем увавший ранее. Как видим, таск «Create a symbolic link» добавлен и успешно выполнен, а потом успехом завершился и таск «Run python script».

Сравнение со встроенным отладчиком Ansible

Когда мы начали разработку cotea, встроенный отладчик Ansible уже существовал, но обладал меньшим функционалом, чем сейчас. Но до сих пор интерактивный режим cotea в нескольких моментах опережает отладчик Ansible. Вот что мы можем:

  • Добавлять переменную, которая будет доступна всем последующим таскам;

  • Если какой-то таск упал, то можно добавить полностью новый таск (через задание строкового описания, как в плейбуке). Инструмент сначала выполнит добавленный таск который все починит, расцветут цветы и запоют птицы, а затем еще раз попробует запустить таск упавший;

  • Реализовать через интерфейсы полноценный отладчик Ansible в виде плагинов для IDE. Компоненты встроенного отладчика вшиты в исходный код Ansible, и использовать их извне не получится.

Заключение

Инструмент cotea позволяет программно контролировать выполнение Ansible через итерирование по плеям и таскам, получать информацию о ходе выполнения, а также добавлять новые Ansible-сущности в runtime-е (скажем, таски и переменные).

Мы разработали сotea при выполнении различных проектов ИСП РАН. Сейчас мы применяем этот инструмент в составе оркестратора Michman и при развёртывании OpenStack. Хотите узнать больше? Ждем на странице облачных технологий. Читайте наши следующие статьи! Пока.

© Habrahabr.ru