Работа с Galaxy Collections (3 часть серии)
В части 1 этой серии мы узнаем, как разработать и использовать пользовательский плагин в вашем Ansible Playbook. В части 2 мы рассмотрим, как мы сможем подключить этот плагин в неспрагое собрание и автоматически пакет и загружать эту коллекцию на айную галактику.
- Зачем написать пользовательский аналимный плагин?
- Выбор типа плагина
- Написание пользовательского плагина
- Все плагины должны быть написаны в Python
- Все плагины должны повысить ошибки
- Возвращать строки в Unicode
- Написание фактического поведения
- Соответствуйте стандартам конфигурации и документации Ansibiable
- Просмотр документации
- Лицензия
- ДОКУМЕНТАЦИЯ
- ПРИМЕРЫ
- ВЕРНУТЬ
- Используя пользовательский плагин в вашем PlayBook
- Следующие шаги
Зачем написать пользовательский аналимный плагин?
Ошибкие плагины дополняют функциональность основных функций Ansible с логикой и функциями, которые доступны для всех модулей. Создание плагина на самом деле очень просто, однако это требует очень специфических конфигураций в вашем коде плагина, который может заставить плагин на самом деле функционировать намного сложнее.
Плагин, который мы создадим, извлекает последнюю мечее выпуск репозитория GitHub. Наша версия будет работать только против публичных репозиториев GitTub, но вы можете продлить этот плагин для проведения частных репозиториев, принимая токен GitHub как переменную среды ( соответствующую проблему GitHub ). Полный плагин поиска, который мы будем построить, можно найти на Github Отказ
Предположим, что нам нужно запросить API GitHub для последнего меченного выпуска репозитория и использовать этот номер версии в наших задачах. Например, Вот пример Откуда я извлекаю последнюю версию выпуска из GitHub, чтобы загрузить и проверить контрольную сумму для последней версии Terraform от Releasze.hashicorp.com
Отказ
Мы можем выполнить это со следующими задачами:
- name: Terraform | Get latest release uri: url: https://api.github.com/repos/hashicorp/terraform/releases/latest headers: Accept: application/vnd.github.v3+json body_format: json return_content: yes register: terraform_release - name: Terraform | Set version set_fact: # This removes the 'v' from the tag: 'v1.1.0' -> '1.1.0' ansible_version: "{{ terraform_release.json.tag_name[1:] }}"
В качестве альтернативы, если мы хотели сделать это как единую задачу, мы можем сделать это в одной команде с оболочка
(Предполагая наличие JQ
уже установлен на целевом хосте):
- name: Terraform | Get the latest version (will include 'v' in the tag name) shell: | set -o pipefail curl -H 'Accept: application/vnd.github.v3+json' https://api.github.com/repos/hashicorp/terraform/releases/latest | jq .tag_name args: executable: /bin/bash changed_when: false register: terraform_version
1-2 Задачи не настолько велики на нашу плейбль, но мы должны повторить эти задачи каждый раз, когда мы хотим получить последнюю версию репо. В моем Playbook, который устанавливает мою местную машину развития, я делаю это 5 раз. Вместо того, чтобы написать 5-10 дополнительных задач (в зависимости от того, если вы используете URI
и set_fact
или оболочка
), Я хотел бы просто вызвать плагин в моих других задачах, которые требуют этой версии. С плагином мы можем использовать пользовательский поиск:
- name: Terraform | Get latest release set_fact: terraform_version: "{{ lookup('github_version', 'hashicorp/terraform')[1:] }}"
внутри любой задачи.
Выбор типа плагина
Первое, что нам нужно сделать, это решить Какой тип несчастного плагина Мы хотим создать (спойлер из фрагмента кода выше, мы построим плагин поиска):
- Плагины действия Позвольте вам интегрировать локальную обработку и локальные данные с функциональностью модуля
- Плагины кэширования Хранить собранные факты и данные, полученные Плагины инвентаря
- Плагины обратного вызова Добавьте новые поведения в Anbible при реагировании на события
- Плагины связи разрешать Anbile для подключения к целевым хостам, так что он может выполнять задачи на них
- Плагины фильтра Манипулировать данными
- Плагины инвентаря анализируйте источники инвентаризации и образуют представление в памяти инвентаризации
- Плагины поиска Потяните данные из внешних хранилище данных
- Тестовые плагины Проверьте данные
- Vars плагины Включить дополнительные вариабельные данные в несбыточные прогоны, которые не произошли от источника инвентаризации, PlayBook или командной строки
Мы хотим поразить Github выпускает API И анализировать последнюю версию тегов выпуска с вывода. Это звучит как Плагин поиска Отказ Эти плагины извлекают данные из файловой системы, а также «внешние хранилища данных и услуг».
Аналогичное руководство по развитию включает Образец реализации Из каждого типа плагина мы можем использовать в качестве шаблона в нашем пользовательском плагине. К сожалению, этот код шаблона не правильно устанавливает пользовательский плагин. Существуют дополнительные требования, распространяемые в документации по несмешкой. Итак, давайте совместим все требования и посмотрите, как построить наш плагин.
Написание пользовательского плагина
Вот …| образец плагина поиска Долбатим от документации по несмешкой:
# python 3 headers, required if submitting to Ansible from __future__ import (absolute_import, division, print_function) __metaclass__ = type DOCUMENTATION = """ lookup: file author: Daniel Hokka Zakrissonversion_added: "0.9" short_description: read file contents description: - This lookup returns the contents from a file on the Ansible controller's file system. options: _terms: description: path(s) of files to read required: True notes: - if read in variable context, the file can be interpreted as YAML if the content is valid to the parser. - this lookup does not understand globing --- use the fileglob lookup instead. """ from ansible.errors import AnsibleError, AnsibleParserError from ansible.plugins.lookup import LookupBase from ansible.utils.display import Display display = Display() class LookupModule(LookupBase): def run(self, terms, variables=None, **kwargs): # lookups in general are expected to both take a list as input and output a list # this is done so they work with the looping construct 'with_'. ret = [] for term in terms: display.debug("File lookup term: %s" % term) # Find the file in the expected search path, using a class method # that implements the 'expected' search path for Ansible plugins. lookupfile = self.find_file_in_search_path(variables, 'files', term) # Don't use print or your own logging, the display class # takes care of it in a unified way. display.vvvv(u"File lookup using %s as file" % lookupfile) try: if lookupfile: contents, show_data = self._loader._get_file_contents(lookupfile) ret.append(contents.rstrip()) else: # Always use ansible error classes to throw 'final' exceptions, # so the Ansible engine will know how to deal with them. # The Parser error indicates invalid options passed raise AnsibleParserError() except AnsibleParserError: raise AnsibleError("could not locate file in lookup: %s" % term) return ret
Давайте распадаем, что здесь происходит. Существуют определенные требования, которые должны следовать все плагины. Они должны:
- Будьте написаны в Python (поддерживая как Python 2 и 3)
- Повышать ошибки
- Возвращать строки в Unicode
- Соответствуйте стандартам конфигурации и документации Ansibiable
Мы вернемся к последнему моменту, когда образец Anisible Plugin предоставляет выше, не включает всю необходимую конфигурацию и документацию.
Все плагины должны быть написаны в Python
Плагины должны поддерживать любые версии Python Versions Ansible. По состоянию на публикацию этой статьи, это Python 2.7 и 3.5+. Их текущие требования можно найти здесь Отказ
Чтобы поддержать как Python 2 и 3 в вашем пользовательском платене, вы должны установить следующие заголовки:
from __future__ import (absolute_import, division, print_function) __metaclass__ = type
В Python 2, декларация __metaclass__
Рассказывает Python 2 для создания классов нового стиля (что по умолчанию в Python 3). Дополнительная информация о классах старого стиля против нового стиля можно найти здесь Отказ
Плагины должны быть в состоянии загружаться с помощью Anisible Плагин
, что означает, что плагин, который вы создаете, должен быть * Модуль
Класс, который наследует тип базового класса любого плагина, который вы хотите построить. Для плагина поиска, то есть Прозвегрупкость
и Назамник
Отказ Вы можете получить это из образца модуля любого типа плагина, который вы создаете.
from ansible.plugins.lookup import LookupBase class LookupModule(LookupBase):
Кроме того, все импорт Должен быть в форме из <имя> Импорт <Определенные вещи>
Отказ Импорт не разрешается использовать подстановочный знак: от __ Импорт *
Отказ Вы должны быть явным и импортировать только минимум вашего плагина. Это задокументировано здесь Внутри «Развивающиеся модули» раздел неспраговой документации. Большая часть этой документации распространяется на пользовательские плагины, но не все это (например, плагины не должны включать в себя комментарию Python Shebang или UTF-8, как модули должны). Простой!
Обратите внимание, что импорт пользовательского плагина перечислены после Переменные документации в верхней части файла. Это ломает льминальное правило E402 Что говорится, что все импорт уровня модуля должны быть в верхней части файла. Аналимые нагрузки на заказ модуль Конфигурация Импорт в верхней части файла (например, __futures__
Импорт), но явно желает, чтобы весь конкретный модуль импорт после переменных документации. Это должно сохранить в зависимости от кода-в зависимости от фактического кода, а не разделяться строками документации. Поскольку цель отвлечения руководящих принципов состоит в том, чтобы улучшить читаемость, Anisible сделал этот выбор игнорировать E402 для улучшения читаемости их модулей.
Все плагины должны повысить ошибки
Любые ошибки, вызванные внутри вашего плагина, должны поднять AnsibleError
Отказ AnsibleError
является базовым классом исключения, предоставленного Anisible. Специфические типы ошибок можно найти в Этот файл Отказ Например, AnsibleParserError
Может использоваться для ошибок, читающих вход, предоставляемый в плагин. Это должно разрешить возможность обрабатывать ошибки, однако он был настроен в конкретной PlayBook. Поскольку мы создаем плагин поиска, давайте использовать Ansiblelookuperror
Для наших общих поднятых исключениях.
Важно : При упаковке ошибок внутри AnsibleError
Вы должны использовать to_native ()
Функция от Anbible. Это обеспечивает правильную совместимость строки между версиями Python. Это не упоминается в примерном коде плагина:)
Обратите внимание, что to_native
исходит от защищенного члена Ansible.module_utils .__ текст
Отказ Обычно вы не должны использовать защищенный элемент другого класса в вашем коде. Тем не менее, это то, что Anisible хочет, чтобы мы делали ¯ \ _ (ツ) _/¯.
from ansible.errors import AnsibleLookupError from ansible.module_utils._text import to_native from json import JSONDecodeError, loads try: # Load some response content into JSON json_response = loads(response.read().decode("utf-8")) version = json_response.get("tag_name") except JSONDecodeError as e: raise AnsibleLookupError("Error parsing JSON from Github API response: %s" % to_native(e))
Возвращать строки в Unicode
Вы должны преобразовать любые строки, возвращаемые своим плагином в тип Unicode Python. Это гарантирует, что строки могут быть обработаны Jinja2. Чтобы преобразовать строку, используйте:
from ansible.module_utils._text import to_text result_string = to_text(result_string)
Опять же, to_text
исходит от защищенного члена Ansible.module_utils .__ текст.
Опять же, это то, что Anisible хочет, чтобы мы делали Отказ
Написание фактического поведения
Мы, наконец, можем обратиться к нашему Беги
Функция и настроить поведение этого плагина. Нам нужно сохранить определение функции:
def run(self, terms, variables=None, **kwargs):
Мы также должны вернуть список:
versions = [] # ... do things and append items to the list return versions
Плагин поиска должен принять список в качестве ввода ( Условия
Может быть списком) и вывод списка. Это для того, чтобы плагин поддерживать с_ *
петли внутри несютимой задачи.
Что мы хотим сделать, это посмотреть на последнюю версию выпуска для каждого репо, переданного в наше термины
. Для читабельности я переименовал Условия
к Репо
Отказ Я ожидаю, что список в любом случае, поэтому я могу переименовать переменную, однако я выбираю. Конвенция с Anbible — покинуть имя переменной Условия
, Однако.
Давайте начнем с подтверждения нашего ввода. Во-первых, нам нужно потерпеть неудачу, если мы не получим никаких имен репо.
versions = [] if len(repos) == 0: raise AnsibleParserError("You must specify at least one repo name")
Во-вторых, давайте проверим, что каждое репо, которое мы получаем, правильно отформатированы в соответствии с именем пользователя GitHub и Refo Name Guidelins.
from re import compile as regex_compile #... for repo in repos: # https://regex101.com/r/CHm7eZ/1 valid_github_username_and_repo_name = regex_compile(r"[a-z\d\-]+\/[a-z\d\S]+") if not repo or not valid_github_username_and_repo_name.match(repo): raise AnsibleParserError("repo name is incorrectly formatted: %s" % to_text(repo))
Остальная часть кода будет происходить в пределах Для репо в Repos
блокировать. При работе с сетевыми запросами в Python я обычно использую Запросы библиотека. Тем не менее, это 3-й импорт библиотеки, который обескуражен неслыманным. Вместо этого мы хотим использовать стандартную библиотеку урлыб
. Тем не менее, Урлыб
только поддерживает Python 3+. Версия Python 2.x — Urllib2
Отказ Таким образом, мы можем импортировать только один, но наш неизбежный модуль должен поддерживать обе версии. Чтобы обойти это, Anisible предоставляет нам независимую версию Урлыб
, Ansible.module_utils.urls.
.
В качестве файла состояния:
URL Модуль Utils предлагает замену для библиотеки Python Urllib2 Python.
URLLIB2 — это способ Python STDLIB для извлечения файлов из Интернета, но ему не хватает некоторых функций безопасности (вокруг проверки сертификатов SSL), которые пользователи должны заботиться о большинстве ситуаций. Использование функций в этом модуле исправляет недостатки в модуле Urllib2, где это возможно.
Есть также сторонние библиотеки (например, запросы), которые можно использовать для замены URLLIB2 с более безопасной библиотекой. Однако все сторонние библиотеки требуют, чтобы библиотека была установлена на управляемой машине. Это дополнительный шаг для пользователей, использующих модуль. Если возможно, избегайте сторонних библиотек, используя этот код вместо этого.
Итак, наш HTTP-запрос будет выглядеть так:
from ansible.module_utils.urls import open_url # ... response = open_url( "https://api.github.com/repos/%s/releases/latest" % repo, headers={"Accept": "application/vnd.github.v3+json"}, )
Один бонус к этому это то, что Ansible.module_utils.urls
Похоже, что для нас обрабатывает запросы ошибок для нас, поэтому нам не нужно беспокоиться о обработке ошибок для HttpExceptions.
. Теперь мы можем извлечь наш тег JSON Release, проверяя, чтобы убедиться, что ошибки не происходят:
from json import JSONDecodeError, loads # ... try: json_response = loads(response.read().decode("utf-8")) version = json_response.get("tag_name") if version is not None and len(version) != 0: versions.append(version) else: raise AnsibleLookupError( "Error extracting version from Github API response:\n%s" % to_text(response.text) ) except JSONDecodeError as e: raise AnsibleLookupError("Error parsing JSON from Github API response: %s" % to_native(e))
Код полного плагина поиска сейчас:
from ansible.errors import AnsibleLookupError, AnsibleParserError from ansible.plugins.lookup import LookupBase from ansible.utils.display import Display from ansible.module_utils._text import to_native, to_text from ansible.module_utils.urls import open_url from json import JSONDecodeError, loads from re import compile as regex_compile display = Display() class LookupModule(LookupBase): def run(self, repos, variables=None, **kwargs): # lookups in general are expected to both take a list as input and output a list # this is done so they work with the looping construct 'with_'. versions = [] if len(repos) == 0: raise AnsibleParserError("You must specify at least one repo name") for repo in repos: # https://regex101.com/r/CHm7eZ/1 valid_github_username_and_repo_name = regex_compile(r"[a-z\d\-]+\/[a-z\d\S]+") if not repo or not valid_github_username_and_repo_name.match(repo): # The Parser error indicates invalid options passed raise AnsibleParserError("repo name is incorrectly formatted: %s" % to_text(repo)) display.debug("Github version lookup term: '%s'" % to_text(repo)) # Retrieve the Github API Releases JSON try: # ansible.module_utils.urls appears to handle the request errors for us response = open_url( "https://api.github.com/repos/%s/releases/latest" % repo, headers={"Accept": "application/vnd.github.v3+json"}, ) json_response = loads(response.read().decode("utf-8")) version = json_response.get("tag_name") if version is not None and len(version) != 0: versions.append(version) else: raise AnsibleLookupError( "Error extracting version from Github API response:\n%s" % to_text(response.text) ) except JSONDecodeError as e: raise AnsibleLookupError("Error parsing JSON from Github API response: %s" % to_native(e)) display.vvvv(u"Github version lookup using %s as repo" % to_text(repo)) return versions
Таким образом, мы написали наш пользовательский код плагина, и мы сделаем, правильно? На самом деле, мы теперь должны правильно документировать этот плагин для непредвиденных инструментов, чтобы успешно обработать его.
Соответствуйте стандартам конфигурации и документации Ansibiable
Мы обсудили несколько требований к конфигурации в вышеупомянутых разделах. Давайте поговорим о том, как Anisible требует документированного плагина. Эти строки документации должны быть отформатированы как действительный YAML. Anisible будет разбирать эти переменные документации для отображения инструкций по использованию и помощи. Если вы создаете только плагин для вашего личного игрового магазина, они не требуются. Однако они настоятельно рекомендуются и являются Требуется, если вы хотите отправить запрос на потяжку, чтобы получить ваш плагин в Anisible/Anisible
Отказ Если вы хотите загрузить свой плагин к Ansible Galaxy, это сильно Рекомендуется использовать эти стандарты документации, чтобы помочь другим в использовании вашего плагина.
Важно : Переменные документации должны быть правильным синтаксисом YAML. Вы можете проверить, правильно ли ваша документация отформатирована, запуская Ansible-doc
Команда описана ниже на вашем плагине. Если документация отображается ваши переменные, правильно отформатированы.
Просмотр документации
Вы можете просмотреть документацию по любому плагину через Ansible-doc -t <Тип> <имя плагина>
Отказ Если вы используете локальный файл плагин, не установленный на путь в Ansible_lookup_plugins Переменная, вы можете указать Ansible-doc
к вашему плагину через:
ANSIBLE_LOOKUP_PLUGINS=<./local/path/to/plugin/directory> ansible-doc -t
Например, если вы написали этот плагин в каталоге в вашем проекте с именем lookup_plugins/
Вы бы позвонили:
ANSIBLE_LOOKUP_PLUGINS=./lookup_plugins ansible-doc -t lookup github_version
Лицензия
На самом деле это должно отправить ваш документ до переменных документации. Это требуется для того, чтобы любой плагин был принят в Anisible Core Repo и настоятельно рекомендуется для всех файлов, загруженных в аварийную галактику.
# (c) 2019, Ari Kalfus# MIT License (see LICENSE)
Вы можете использовать любую лицензию, которую вы хотите, однако некоторые не могут быть приняты в неспособное ядро. Обязательный, как правило, использует GPL-3.0. В моем плагине я выбрал MIT и ссылаться на Лицензия
Файл в моем репозитории плагина. Вы также должны включать в себя год, который вы создали плагин, ваше имя и адрес электронной почты (необязательный, но рекомендуется) в заголовке авторских прав выше лицензии.
Как правило, вы не включаете полный текст лицензии в вашем файле, так как это может быть довольно долго. Вместо этого вы перечисляете какой тип лицензии, которую вы вызываете (MIT, GPL-3,0 и т. Д.), И ссылаетесь на другой файл, где находится полная лицензия. Вы также можете ссылаться на URL, столько несовмебных файлов DO ( https://www.gnu.org/licenses/gpl-3.0.txt в этих случаях).
ДОКУМЕНТАЦИЯ
Ваш плагин должен иметь Документация
Переменная. Документация
Переменная описана подробно здесь Отказ Все поля требуются, если эта документация явно не говорит иначе. Давайте посмотрим на документацию для github_version
плагин:
DOCUMENTATION = r""" # Include the type of plugin (lookup) and the name that will be invoked in a playbook (github_version) lookup: github_version # A list of authors who contributed to this file. # You can optionally add Github username (suggested) and email (legacy suggested). I opted for both. author: - Ari Kalfus (@artis3n)# In what version of Ansible this plugin was added. # If merging a plugin to the ansible/ansible core repo this must be the next non-frozen unreleased version of Ansible. # Otherwise it doesn't really matter, however I recommend using the next unreleased (not necessarily non-frozen) Ansible version when the module was created. version_added: "2.9" # Any Python package requirements for this module. It is STRONGLY recommended that you avoid all 3rd party library packages. # If you intend to merge into ansible/ansible, your PR will likely be rejected if it uses 3rd party library packages. # This is to keep the dependency requirements of Ansible small. requirements: - json - re # A few words describing the plugin. This will be displayed from `ansible-doc -l` (list). short_description: Get the latest tagged release version from a public Github repository. # A few complete sentences describing the plugin. description: - This lookup returns the latest tagged release version of a public Github repository. - A future version will accept an optional Github token to allow lookup of private repositories. # The parameters or arguments to the plugin. # Use an empty dictionary ({ }) if the plugin takes no arguments. # All options used by the plugin should be thoroughly documented. options: # The name of the option repos: # Description of the option description: A list of Github repositories from which to retrieve versions. # This means this option must be supplied or the plugin will fail. # You must validate the content of the variable yourself in the code. required: True # You will likely also use the following: # default: Mutually exclusive with `required`. Document the default value the plugin will use for this option. You must ensure your function sets this default value. # choices: A list of options if only certain specific values are accepted by this option. # type: Specify an argspec-compliant type for this option. # suboptions: If this option is a dict, you can specify its contents via this attribute. # Full sentences with any additional information about this module. notes: - The version tag is returned however it is defined by the Github repository. - Most repositories used the convention 'vX.X.X' for a tag, while some use 'X.X.X'. - Some may use release tagging structures other than semver. - This plugin does not perform opinionated formatting of the release tag structure. - Users should format the value via filters after calling this plugin, if needed. # Any additional documentation can be linked via this attribute. seealso: - name: Github Releases API description: API documentation for retrieving the latest version of a release. link: https://developer.github.com/v3/repos/releases/#get-the-latest-release """
ПРИМЕРЫ
Ваш плагин должен иметь Примеры
Переменная. Примеры
Переменная описана подробно здесь Отказ Включите несколько примеров, демонстрирующих, как использовать свой плагин.
EXAMPLES = r""" - name: Get the latest version, also strip the 'v' out of the tag version, e.g. 'v1.0.0' -> '1.0.0' set_fact: ansible_version: "{{ lookup('github_version', 'ansible/ansible')[1:] }}" - name: Operate on multiple repositories git: repo: https://github.com/{{ item }}.git version: "{{ lookup('github_version', item) }}" dest: "{{ lookup('env', 'HOME') }}/projects" with_items: - ansible/ansible - ansible/molecule - ansible/awx """
ВЕРНУТЬ
Ваш плагин должен иметь Вернуть
Переменная. Вернуть
Переменная описана подробно здесь Отказ В этом разделе документируются, какая информация ваш плагин возвращается для использования другими модулями. В качестве стандартного плагина поиска мы возвращаем список.
RETURN = r""" _list: description: - List of latest Github repository version(s) type: list """
Используя пользовательский плагин в вашем PlayBook
Теперь, когда мы написали наш плагин, как мы скажем это неизбежно импортировать его в наш PlayBook? Есть определенные « Magic Ciredies » Anisible будет автоматически искать для импорта локальных модулей и плагинов. Для плагинов Anisible ищет локальный каталог имени этого типа плагина (например, lookup_plugins/
). Мы можем поставить наши github_version.py
плагин под lookup_plugins/
Каталог в нашем корне и Ansible Project и Anisible будет автоматически импортировать его и сделать его доступным внутри нашего контекста Playbook.
Предполагая, что наш плагин находится в lookup_plugins/github_version.py
В нашем корне на нашем проекте мы теперь можем использовать его:
- name: Testing new plugin debug: msg: "Terraform's latest version: {{ lookup('github_version', 'hashicorp/terraform')[1:] }}"
Следующие шаги
Теперь, когда мы написали наш пользовательский плагин, что дальше? Мы можем упаковать наш плагин в Аналимая коллекция И загрузите его к айной галактике для других пользователей, чтобы импортировать в их детски. Это требует некоторых незначительных рефакторов на нашему настроек плагина, которую мы обсудим в Часть 2 этой серии. Мы также обсудим, как построить Действие GitHub И используйте тот, который я создал для автоматической связки вашей коллекции и загрузить его на айсовную галактику.
Работа с Galaxy Collections (3 часть серии)
Оригинал: «https://dev.to/artis3n/galaxy-collections-part-1-extending-ansible-through-custom-plugins-24ma»