В апреле 2023 года разработчики --- - name: Say hello to user ansible.builtin.debug: msg: "Hello, dear user"
Для написания драйвера необходим Python не старее версии 3.10 и Molecule от версии 5.0.1 и выше. Также понадобятся базовые знания Ansible. Так как плагин будет работать с Docker, необходимо обеспечить безрутовый доступ для команд. Информацию по настройке можно найти на официальной странице Docker. Разработку можно разделить на несколько этапов: Создать скелет проекта внутри Добавить логику для создания платформ через драйвер Реализовать механизмы для выполнения команд Поехали! Инициализируем пустой репозиторий в директории Опишем базовый класс нашего драйвера, который наследуется от класса molecule.api.Driver, переопределим конструктор. Создадим геттеры и сеттеры для поля src/molecule_custom_docker/driver.py За основу для служебных файлов setup.cfg pyproject.toml После добавления TOML и CFG-файлов код драйвера можно упаковать в Python-пакет, а сам драйвер custom_docker становится допустимым для использования в Molecule. Проверим: Проверим возможность создания сценария: Ошибка говорит о том, что Molecule не может найти директорию шаблона и ищет этот шаблон в подкаталоге cookiecutter. CookieCutter — это библиотека и CLI-утилита, которая позволяет создавать проекты из шаблонов, используя Jinja2—шаблоны под капотом. CookieCutter-шаблоном является директория или репозиторий, где на верхнем уровне располагается файл cookiecutter.json — конфигурационный файл. Директория с конфигурационным файлом называется корнем шаблона. Внутри конфигурационного файла описываются правила генерации, подключаются дополнительные модули, а также перечисляются переменные, которые могут быть использованы не только внутри файлов, но и для имен файлов и каталогов шаблона. Обращение к переменным возможно через использование префикса Создадим шаблон CookieCutter для драйвера: В конфигурационном файле src/molecule_custom_docker/cookiecutter/cookiecutter.json После инициализации переменных, при вызове генерации через CookieCutter, все строки вида Значение Добавим необходимые директории и файлы в корень шаблона: Проверим возможность создания сценариев с использованием шаблонов: Рассмотрим сценарий default. Внутри содержатся два файла: конфигурационный файл сценария Molecule, а также плейбук для проверки. molecule/default/molecule.yml Данный файл создался на основе полей из команды src/molecule_custom_docker/cookiecutter/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/molecule.yml src/molecule_custom_docker/cookiecutter/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/converge.yml Создадим сценарий Для проверки работы команд src/molecule_custom_docker/playbooks/create.yml src/molecule_custom_docker/playbooks/destroy.yml Проверим создание и удаление платформ в сценарии Ошибка возникает на задаче удаления файлов из временной директории сценария. По умолчанию список сохраняемых файлов не инициализирован, что вызывает ошибку при соединении списков. Избавимся от ошибки, переопределив метод default_safe_files: src/molecule_custom_docker/driver.py После исправления файла src/molecule_custom_docker/playbooks/create.yml src/molecule_custom_docker/playbooks/destroy.yml Для создания контейнеров используется коллекция src/molecule_custom_docker/driver.py Версия коллекции, указываемая в этой функции, является минимально допустимой. Все Ansible-зависимости драйвера устанавливаются на этапе dependency при выполнении сценария. Проверим получение зависимостей и создание платформы для сценария Вывод аналогичен и для удаления платформ. На данный момент команды Проверим возможность «прогона» роли По умолчанию Molecule будет пытаться подключиться к созданным платформам через SSH. Изменим это поведение, переопределив метод ansible_connections_options, где сменим плагин для подключения с SSH на src/molecule_custom_docker/driver.py После добавления converge на хостах выполняется успешно: Ура! Создаваемые драйвером платформы стали доступны для тестирования. Каждый драйвер Molecule позволяет пользователю зайти на созданную платформу, и src/molecule_custom_docker/driver.py Переменная Обе функции отвечают за работоспособность команды Итог: теперь драйвер поддерживает команду molecule_login. В случаях, когда значения параметров создаваемой платформы определяются на этапе инициализации, следует использовать отдельный конфигурационный файл. Изменим логику таким образом, чтобы для команды login использовался ID контейнера, а не его имя. Доработаем плейбук создания платформы. Добавим задачи поиска ID, а также генерации конфига в конец плейбука: src/molecule_custom_docker/playbooks/create.yml Теперь при выполнении команды ~/.cache/molecule/hello_world/test_c/instance_config.yml Значение для ключа ID вычисляется в ходе работы Ansible-плейбука и недоступно для использования внутри Python-кода. Путь до файла Доработаем драйвер, чтобы при src/molecule_custom_docker/driver.py Итог: при вызове команды Дальше нас ждет почти бесконечный цикл внедрения новых фич и оптимизации уже написанного кода. В процессе дальнейшей разработки и эксплуатации я призываю помнить, что: Окружение пользователя может отличаться. Рано или поздно возникнет пользователь, у которого нет даже базовых коллекций для работы Ansible или какой-либо библиотеки Python. Так было у нас. Следите за тем, что импортируется, запускается и загружается. Внутри драйвера используется библиотека Python? Укажите ее в зависимостях к пакету. Используются Ansible-модули, которые начинаются не с Лучше использовать временные директории. Вместо того чтобы формировать шаблоны для виртуальной машины через lookup-модуль, мы сохраняем сгенерированный файл во временную директорию. Это не раз спасало нас в ходе разработки и отладки новых фич для плагина. Спасибо за внимание! Исходный код для molecule_custom_docker вы найдете в репозитории на GitHub. Тексты, которые могут вас заинтересовать: → Как ограничить количество выполняющихся задач в Jenkins при вызове parallel: сравниваем решения → История печатных плат: от Эйслера до наших дней → Простые правила, которые помогают писать на Go без побочных эффектовНачало разработки
molecule_custom_driver, настроить сборку Python-пакета.custom_docker. Добавить поддержку команд molecule create и molecule destroy.molecule converge и molecule login.molecule_custom_docker, создадим скелет проекта для драйвера:. ├── .git ├── .gitignore ├── pyproject.toml ├── setup.cfg ├── src │ └── molecule_custom_docker │ ├── driver.py │ └── __init__.py └── venv_name. Без этого Molecule не сможет получать информацию о кастомном драйвере:from molecule.api import Driver from molecule import logger LOG = logger.get_logger(__name__) class CustomDocker(Driver): def __init__(self, config=None): super(CustomDocker, self).__init__(config) self._name = "custom_docker" @property def name(self): return self._name @name.setter def name(self, value): self._name = valuepyproject.toml и setup.cfg возьмем существующие в molecule_docker:[metadata] name = molecule_custom_docker description = Molecule Custom Docker Plugin :: run molecule tests on Docker images classifiers = Development Status :: 2 - Pre-Alpha Environment :: Console Intended Audience :: Developers Intended Audience :: Information Technology Intended Audience :: System Administrators Natural Language :: English Operating System :: OS Independent Programming Language :: Python :: 3 Programming Language :: Python :: 3.10 Topic :: System :: Systems Administration Topic :: Utilities keywords = ansible roles testing molecule plugin docker [options] use_scm_version = True python_requires = >=3.10 package_dir = = src packages = find: include_package_data = True zip_safe = False install_requires = molecule >= 5.0.1 [options.entry_points] molecule.driver = custom_docker = molecule_custom_docker.driver:CustomDocker [options.packages.find] where = src[build-system] requires = [ "pip >= 19.3.1", "setuptools >= 42", "setuptools_scm[toml] >= 3.5.0", "setuptools_scm_git_archive >= 1.1", "wheel >= 0.33.6", ] build-backend = "setuptools.build_meta" [tool.setuptools_scm] local_scheme = "no-local-version"# Выполняется в директории molecule_custom_docker > cd ../hello_world > source venv/bin/activate > pip install -e ../molecule_custom_docker/ > molecule drivers ────────────────────── custom_docker delegatedРабота с CookieCutter
# Выполняется в директории hello_world > molecule init scenario -d custom_docker default INFO Initializing new scenario default... CRITICAL The specified template directory (/home/pavel/hello_world/venv/lib/python3.10/site-packages/molecule/cookiecutter/scenario/driver/custom_docker) does not existcookiecutter: {{ cookiecutter.<variable_name> }}.src/molecule_custom_docker ├── cookiecutter │ └── cookiecutter.json ├── driver.py └── __init__.pycookiecutter.json инициализируем следующие переменные:{ "molecule_directory": "molecule", "dependency_name": "OVERRIDDEN", "driver_name": "OVERRIDDEN", "provisioner_name": "OVERRIDDEN", "scenario_name": "OVERRIDDEN", "role_name": "OVERRIDDEN", "verifier_name": "OVERRIDDEN" }"{{ cookiecutter.molecule_directory }}" будут заменены на "molecule", "{{ cookiecuter.dependency_name }}" на "OVERRIDDEN" и так далее."OVERRIDDEN" не является служебным. Оно используется для обозначения тех полей, которые будут перезаписаны при вызове CookieCutter из Molecule. Конкретные значения будут вычислены динамически на основе флагов, переданных команде molecule init scenario. Например, значение dependency_name будет взято из флага --dependency-name, driver_name — из флага --driver-name. Напрямую, без указания в cookiecutter.json, значения из Molecule невозможно использовать в CookieCutter-шаблоне.src/molecule_custom_docker ├── cookiecutter │ ├── cookiecutter.json │ └── {{cookiecutter.molecule_directory}} │ └── {{cookiecutter.scenario_name}} ├── driver.py └── __init__.py# Выполняется в директории hello_world > molecule init scenario -d custom_docker INFO Initializing new scenario default... INFO Initialized scenario in /home/pavel/hello_world/molecule/default successfully. > molecule init scenario -d custom_docker test_a INFO Initializing new scenario test_a... INFO Initialized scenario in /home/pavel/hello_world/molecule/test_a successfully. > molecule init scenario -d custom_docker test_b INFO Initializing new scenario test_b... INFO Initialized scenario in /home/pavel/hello_world/molecule/test_b successfully. > tree ./molecule ./molecule ├── default │ ├── molecule.yml │ └── verify.yml ├── test_a │ ├── molecule.yml │ └── verify.yml └── test_b ├── molecule.yml └── verify.yml--- dependency: name: galaxy driver: name: custom_docker platforms: - name: instance provisioner: name: ansible verifier: name: ansiblemolecule init scenario и шаблона в основном репозитории Molecule. Добавим собственные шаблоны для файлов внутри директории сценария. Они будут доступны для редактирования пользователем. --- dependency: name: {{cookiecutter.dependency_name}} driver: name: {{cookiecutter.driver_name}} platforms: - name: molecule-{{cookiecutter.role_name}} image: python tag: 3.10-slim-buster provisioner: name: {{cookiecutter.provisioner_name}} verifier: name: {{cookiecutter.verifier_name}}--- - name: Converge hosts: all gather_facts: true tasks: - name: Import role ansible.builtin.import_role: name: {{cookiecutter.role_name}}test_c, который будет использовать плагин custom_docker и собственный шаблон:# Выполняется в директории hello_world > molecule init scenario -d custom_docker test_c INFO Initializing new scenario test_c... INFO Initialized scenario in /home/pavel/hello_world/molecule/test_c successfully. > tree ./molecule/test_c ./molecule/test_c ├── converge.yml ├── molecule.yml └── verify.yml > cat ./molecule/test_c/molecule.yml --- dependency: name: galaxy driver: name: custom_docker platforms: - name: molecule-hello_world image: python tag: 3.10-slim-buster provisioner: name: ansible verifier: name: ansiblemolecule create и molecule destroy добавим плейбуки create.yml, destroy.yml. При инициализации сценария они не будут отображаться. Плейбуки должны располагаться в директории playbooks:--- - name: Create hosts: localhost connection: local gather_facts: false no_log: "{{ molecule_no_log }}" tasks: - name: Simulate instance creation ansible.builtin.debug: msg: Creating instance--- - name: Destroy hosts: localhost connection: local gather_facts: false no_log: "{{ molecule_no_log }}" tasks: - name: Destroying instance ansible.builtin.debug: msg: "Destroying instance"test_c:> molecule create -s test_c PLAY [Create] ****************************************************************** TASK [Simulate instance creation] ********************************************** ok: [localhost] => { "msg": "Creating instance" } PLAY RECAP ********************************************************************* localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 > molecule destroy -s test_c ... PLAY [Destroy] ***************************************************************** TASK [Destroying instance] ***************************************************** ok: [localhost] => { "msg": "Destroying instance" } PLAY RECAP ********************************************************************* localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 INFO Pruning extra files from scenario ephemeral directory Traceback (most recent call last): ... File "/home/pavel/hello_world/venv/lib/python3.10/site-packages/molecule/driver/base.py", line 147, in safe_files return self.default_safe_files + self._config.config["driver"]["safe_files"] TypeError: unsupported operand type(s) for +: 'NoneType' and 'list'from molecule.api import Driver from molecule import logger import os LOG = logger.get_logger(__name__) class CustomDocker(Driver): def __init__(self, config=None): super(CustomDocker, self).__init__(config) self._name = "custom_docker" @property def name(self): return self._name @name.setter def name(self, value): self._name = value @property def default_safe_files(self): # Возврат пустого списка ведёт к удалению всех файлов из временной директории return []driver.py удаление платформ проходит без ошибок. Реализуем создание и удаление Docker-контейнеров в плейбуках create и destroy соответственно:Развернуть исходный код
--- - name: Create hosts: localhost connection: local gather_facts: false no_log: "{{ molecule_no_log }}" tasks: - name: Create requested Docker instances community.docker.docker_container: name: "{{ item.name }}" hostname: "{{ item.name }}" image: "docker.io/library/{{ item.image }}:{{ item.tag }}" state: started command: "bash -c 'while true; do sleep 10000; done'" register: platform with_items: "{{ molecule_yml.platforms }}" loop_control: label: "{{ item.name }}" async: 7200 poll: 0 - name: Wait for instances creation to complete ansible.builtin.async_status: jid: "{{ item.ansible_job_id }}" register: docker_jobs until: docker_jobs.finished retries: 100 with_items: "{{ platform.results }}"Развернуть исходный код
--- - name: Destroy hosts: localhost connection: local gather_facts: false no_log: "{{ molecule_no_log }}" tasks: - name: Destroy created Docker instances community.docker.docker_container: name: "{{ item.name }}" state: absent register: platform with_items: "{{ molecule_yml.platforms }}" loop_control: label: "{{ item.name }}" async: 7200 poll: 0 - name: Wait for instances deletion to complete ansible.builtin.async_status: jid: "{{ item.ansible_job_id }}" register: docker_jobs until: docker_jobs.finished retries: 100 with_items: "{{ platform.results }}"community.docker, которая может не существовать у конечного пользователя. Чтобы определить коллекцию как Ansible-зависимость драйвера, переопределим метод required_collections:class CustomDocker(Driver): ... @property def required_collections(self): return {"community.docker": "3.4.6"}test_c:> molecule create -s test_c ... INFO Running test_c > dependency INFO Running from /home/pavel/hello_world : ansible-galaxy collection install -vvv community.docker:>=3.4.6 ... INFO Running test_c > create PLAY [Create] ****************************************************************** TASK [Create requested Docker instances] *************************************** changed: [localhost] => (item=molecule-hello_world) TASK [Wait for instances creation to complete] ********************************* changed: [localhost] => (item={'failed': 0, 'started': 1, 'finished': 0, 'ansible_job_id': 'j538221924929.3055431', 'results_file': '/home/pavel/.ansible_async/j538221924929.3055431', 'changed': True, 'item': {'image': 'python', 'name': 'molecule-hello_world', 'tag': '3.10-slim-buster'}, 'ansible_loop_var': 'item'}) PLAY RECAP ********************************************************************* localhost : ok=2 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 INFO Running test_c > prepare WARNING Skipping, prepare playbook not configured.molecule create и molecule destroy выполняются без ошибок. Структура пакета выглядит следующим образом:src/molecule_custom_docker/ ├── cookiecutter │ ├── cookiecutter.json │ └── {{cookiecutter.molecule_directory}} │ └── {{cookiecutter.scenario_name}} │ ├── converge.yml │ └── molecule.yml ├── driver.py ├── __init__.py └── playbooks ├── create.yml └── destroy.ymlИспользуем созданные платформы
hello_wolrd на платформе сценария test_c:> molecule converge -s test_c ... INFO Running test_c > converge PLAY [Converge] **************************************************************** TASK [Gathering Facts] ********************************************************* fatal: [molecule-hello_world]: UNREACHABLE! => {"changed": false, "msg": "Failed to connect to the host via ssh: ssh: Could not resolve hostname molecule-hello_world: Name or service not known", "unreachable": true} PLAY RECAP ********************************************************************* molecule-hello_world : ok=0 changed=0 unreachable=1 failed=0 skipped=0 rescued=0 ignored=0community.docker.docker:class CustomDocker(Driver): ... def ansible_connection_options(self, instance_name): """Опции подключения для Ansible, которые будут передны inventory.""" return {"ansible_connection": "community.docker.docker"}> molecule converge -s test_c ... INFO Running test_c > converge PLAY [Converge] **************************************************************** TASK [Gathering Facts] ********************************************************* ok: [molecule-hello_world] TASK [hello_world : Say hello to user] ***************************************** ok: [molecule-hello_world] => { "msg": "Hello, dear user" } PLAY RECAP ********************************************************************* molecule-hello_world : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0Добавляем поддержку molecule login
custom_docker не должен отличаться. Поддержка этих функций также настраивается в driver.py путем переопределения методов login_options и login_cmd_template. Первая отвечает за добавление опций к подключению, вторая должна возвращать шаблон строки, куда будут добавлены данные для подключения.class CustomDocker(Driver): ... def login_options(self, instance_name): """Набор опций, которые будут использованы для команды login.""" return {"instance": instance_name} @property def login_cmd_template(self): return ( "docker exec " "-e COLUMNS={columns} " "-e LINES={lines} " "-e TERM=bash " "-e TERM=xterm " "-ti {instance} bash" )instance будет добавлена из словаря от функции login_options, columns и lines — из Molecule напрямую. Все ключи, указанные в login_options, доступны для использования внутри login_cmd_template.molecule login. Проверим возможность подключения для сущностей из сценария test_c:> molecule login -s test_c -h molecule-hello_world INFO Running test_c > login root@molecule-hello_world:/# exitБонус: работа с конфигурационным файлом платформы
- name: Gather info about created containers community.docker.docker_container_info: name: "{{ job_result.item.name }}" register: container_info with_items: "{{ platform.results }}" loop_control: loop_var: job_result label: "{{ job_result.item.name }}" - name: Populate instance config ansible.builtin.set_fact: instance_conf_dict: { 'instance': "{{ item.name }}", 'image': "docker.io/library/{{ item.image }}:{{ item.tag }}", 'tag': "{{ item.tag }}", 'ID': "{{ container_info.results[i].container.Id }}" } with_items: "{{ molecule_yml.platforms }}" loop_control: label: "{{ item.name }}" index_var: i register: instance_config_dict - name: Convert instance config dict to a list ansible.builtin.set_fact: instance_conf: "{{ instance_config_dict.results | map(attribute='ansible_facts.instance_conf_dict') | list }}" - name: Dump instance config ansible.builtin.copy: content: "{{ instance_conf | to_json | from_json | to_yaml }}" dest: "{{ molecule_instance_config }}" mode: 0600molecule create будет генерироваться файл ~/.cache/molecule/<role_name>/<scenario_name>/instance_config.yml , в котором есть список словарей, содержащих информацию о созданных платформах.- {ID: e9ec39e81e4f85cff47f6ace3cbaf409dbe7846db307241d94a72fcdec89d108, image: 'docker.io/library/python:3.10-slim-buster, instance: molecule-hello_world, tag: 3.10-slim-buster}instance_config.yml доступен в свойстве Driver._config.driver.instance_config.molecule login использовался ID контейнера:from molecule import util ... class CustomDocker(Driver): ... def _get_instance_config(self, instance_name): """Создаёт генератор со словарями из instance_config.yml""" instance_config_dict = util.safe_load_file(self._config.driver.instance_config) return next( item for item in instance_config_dict if item["instance"] == instance_name ) def login_options(self, instance_name): instance_config = self._get_instance_config(instance_name) return {"id": instance_config["ID"]} @property def login_cmd_template(self): return ( "docker exec " "-e COLUMNS={columns} " "-e LINES={lines} " "-e TERM=bash " "-e TERM=xterm " "-ti {id} bash" )molecule login будет использоваться значение из файла с информацией о платформе.Рабочий прототип драйвера написан и запущен. Что дальше?
ansible.builtin? Добавьте коллекцию в required_collections.README.md спасет вас от потока вопросов в чатах и почте. Например, мы с командой храним в стартовой документации информацию о том, как установить драйвер, какие пакеты нужны для корректной работы, как протестировать роль.
ссылка на оригинал статьи https://habr.com/ru/articles/764906/
Добавить комментарий