Пишем свой драйвер Molecule без костылей и боли

5fae0a19128896453037c95ae12351d0.png

В апреле 2023 года разработчики Molecule представили мажорный релиз инструмента в версии 5.0.0. Помимо множества багфиксов и улучшений различной степени важности, пользователи получили возможность написать свой собственный драйвер, подключить его в уже существующие сценарии тестирования ролей и использовать как molecule.docker или molecule.openstack. Я не нашел или плохо искал статей об этом и решил написать поэтапное руководство по разработке собственного драйвера — от примитивного «Hello world» до работающего прототипа.

В статье вы найдете пример custom_docker доработки оригинального драйвера molecule.docker, описание базовых классов и методов из API Molecule, а также рассказ о нюансах разработки и эксплуатации, с которыми я столкнулся.

Почему с релизом 5.0.0 писать драйверы стало проще

В YADRO мы проводим тестирование ролей и наработок с использованием виртуальных машин. Однако перед созданием ВМ для тестирования роли нам нужно выполнить ряд проверок: подготовить XML-шаблоны, изменить диски. У стандартных драйверов недостаточный для нас функционал, есть вопросы к гибкости и скорости работы. Все это делает невозможным использование стандартных драйверов вроде molecule.libvirt.

До релиза 5.0.0 структура сценария внутри наших ролей выглядела следующим образом:

├── molecule
│   └── default
│   	├── cleanup.yml
│   	├── converge.yml
│   	├── create.yml
│   	├── destroy.yml
│   	├── molecule.yml
│   	├── tasks
│   	│   ├── get-vm-info.yml
│   	│   └── resize.yml
│   	└── templates
│       	└── vm_template.xml.j2

Помимо файлов molecule, converge и verify, в сценарии хранилась вся дополнительная информация для создания платформы: задачи, шаблоны, плейбуки. Пока не было необходимости менять метод для создания виртуальной машины, все эти файлы оставались без исправлений и копировались из роли в роль, из сценария в сценарий.

Это вызывало ряд проблем — как повседневных, так и гипотетических:

  • Для создания новой роли инженеру надо было копировать структуру — вручную или с использованием сторонних инструментов.

  • Повышался порог вхождения. Новоприбывший инженер получал ворох дополнительных файлов, в большинстве случаев с фразой: «Это служебное, менять вряд ли понадобится». Но, если ему встречался какой-то баг, это увеличивало время решения задачи: надо было долго разбираться самому или идти к автору кода.

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

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

Чтобы решить эту задачу для Molecule до версии 5.0.0, нужно было заморочиться. Имя любого драйвера, который мы хотим использовать, должно указываться в JSON-схеме в репозитории Molecule. Поэтому мы должны были не просто описать логику создания и удаления площадкой, но и склонировать репозиторий Molecule, чтобы исправить файл JSON-схемы. А затем хранить локально копии двух репозиториев.

После релиза 5.0.0 этап с клонированием можно пропустить. Если имя кастомного драйвера соответствует одному из указанных шаблонов — molecule-*, molecule_*, custom-*или custom_*, то он автоматически становится доступным для использования. Это позволило нам уменьшить структуру внутри сценария до желаемой:

└── molecule
    └── default
        ├── verify.yml
        ├── converge.yml
        └── molecule.yml

Теперь я расскажу, как писал такой драйвер. Но сначала — немного подготовки.

Старт работы

Для простоты будем делать прототип драйвера для работы с Docker. Это позволит раскрыть почти все тонкости работы Molecule-драйверов. В статье будут упоминаться две директории — molecule_custom_docker и hello_world. Внутри первой будет храниться исходный код для кастомного драйвера, во второй — простая Ansible-роль, которая для тестов Molecule будет использовать новый драйвер.

Содержимое роли hello_world:

./hello_world
└── tasks
    └── main.yml

hello_world/tasks/main.yml

---
- name: Say hello to user
  ansible.builtin.debug:
    msg: "Hello, dear user"

Для написания драйвера необходим Python не старее версии 3.10 и Molecule от версии 5.0.1 и выше. Также понадобятся базовые знания Ansible.

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

Начало разработки

Разработку можно разделить на несколько этапов:

  1. Создать скелет проекта внутри molecule_custom_driver, настроить сборку Python-пакета.

  2. Добавить логику для создания платформ через драйвер custom_docker. Добавить поддержку команд molecule create и molecule destroy.

  3. Реализовать механизмы для выполнения команд molecule converge и molecule login.

Поехали!

Инициализируем пустой репозиторий в директории molecule_custom_docker, создадим скелет проекта для драйвера:

.
├── .git
├── .gitignore
├── pyproject.toml
├── setup.cfg
├── src
│   └── molecule_custom_docker
│   	├── driver.py
│   	└── __init__.py
└── venv

Опишем базовый класс нашего драйвера, который наследуется от класса molecule.api.Driver, переопределим конструктор. Создадим геттеры и сеттеры для поля _name. Без этого Molecule не сможет получать информацию о кастомном драйвере:

src/molecule_custom_docker/driver.py

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 = value

За основу для служебных файлов pyproject.toml и setup.cfg возьмем существующие в molecule_docker:

setup.cfg

[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

pyproject.toml

[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"

После добавления TOML и CFG-файлов код драйвера можно упаковать в Python-пакет, а сам драйвер custom_docker становится допустимым для использования в Molecule. Проверим:

# Выполняется в директории 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 exist

Ошибка говорит о том, что Molecule не может найти директорию шаблона и ищет этот шаблон в подкаталоге cookiecutter.

CookieCutter — это библиотека и CLI-утилита, которая позволяет создавать проекты из шаблонов, используя Jinja2-шаблоны под капотом. CookieCutter-шаблоном является директория или репозиторий, где на верхнем уровне располагается файл cookiecutter.json — конфигурационный файл. Директория с конфигурационным файлом называется корнем шаблона.

Внутри конфигурационного файла описываются правила генерации, подключаются дополнительные модули, а также перечисляются переменные, которые могут быть использованы не только внутри файлов, но и для имен файлов и каталогов шаблона. Обращение к переменным возможно через использование префикса cookiecutter: {{ cookiecutter. }}.

Создадим шаблон CookieCutter для драйвера:

src/molecule_custom_docker
├── cookiecutter
│   └── cookiecutter.json
├── driver.py
└── __init__.py

В конфигурационном файле cookiecutter.json инициализируем следующие переменные:

src/molecule_custom_docker/cookiecutter/cookiecutter.json

{
    "molecule_directory": "molecule",
    "dependency_name": "OVERRIDDEN",
    "driver_name": "OVERRIDDEN",
    "provisioner_name": "OVERRIDDEN",
    "scenario_name": "OVERRIDDEN",
    "role_name": "OVERRIDDEN",
    "verifier_name": "OVERRIDDEN"
}

После инициализации переменных, при вызове генерации через CookieCutter, все строки вида "{{ 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

Рассмотрим сценарий default. Внутри содержатся два файла: конфигурационный файл сценария Molecule, а также плейбук для проверки.

molecule/default/molecule.yml

---
dependency:
  name: galaxy
driver:
  name: custom_docker
platforms:
  - name: instance
provisioner:
  name: ansible
verifier:
  name: ansible

Данный файл создался на основе полей из команды molecule init scenario и шаблона в основном репозитории Molecule. Добавим собственные шаблоны для файлов внутри директории сценария. Они будут доступны для редактирования пользователем.

src/molecule_custom_docker/cookiecutter/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/molecule.yml

 ---
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}}

src/molecule_custom_docker/cookiecutter/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/converge.yml

---
- 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: ansible

Для проверки работы команд molecule create и molecule destroy добавим плейбуки create.yml, destroy.yml. При инициализации сценария они не будут отображаться. Плейбуки должны располагаться в директории playbooks:

src/molecule_custom_docker/playbooks/create.yml

---
- 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

src/molecule_custom_docker/playbooks/destroy.yml

---
- 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'

Ошибка возникает на задаче удаления файлов из временной директории сценария. По умолчанию список сохраняемых файлов не инициализирован, что вызывает ошибку при соединении списков. Избавимся от ошибки, переопределив метод default_safe_files:

src/molecule_custom_docker/driver.py

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 соответственно:

src/molecule_custom_docker/playbooks/create.yml

Развернуть исходный код

---
- 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 }}"

src/molecule_custom_docker/playbooks/destroy.yml

Развернуть исходный код

---
- 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:

src/molecule_custom_docker/driver.py

class CustomDocker(Driver):
    ...
    @property
    def required_collections(self):
        return {"community.docker": "3.4.6"}

Версия коллекции, указываемая в этой функции, является минимально допустимой. Все Ansible-зависимости драйвера устанавливаются на этапе dependency при выполнении сценария. Проверим получение зависимостей и создание платформы для сценария 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=0

По умолчанию Molecule будет пытаться подключиться к созданным платформам через SSH. Изменим это поведение, переопределив метод ansible_connections_options, где сменим плагин для подключения с SSH на community.docker.docker:

src/molecule_custom_docker/driver.py

class CustomDocker(Driver):
 
    ...
 
    def ansible_connection_options(self, instance_name):
        """Опции подключения для Ansible, которые будут передны inventory."""
        return {"ansible_connection": "community.docker.docker"}

После добавления converge на хостах выполняется успешно:

> 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

Каждый драйвер Molecule позволяет пользователю зайти на созданную платформу, и custom_docker не должен отличаться. Поддержка этих функций также настраивается в driver.py путем переопределения методов login_options и login_cmd_template. Первая отвечает за добавление опций к подключению, вторая должна возвращать шаблон строки, куда будут добавлены данные для подключения.

src/molecule_custom_docker/driver.py

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

Итог: теперь драйвер поддерживает команду molecule_login.

Бонус: работа с конфигурационным файлом платформы

В случаях, когда значения параметров создаваемой платформы определяются на этапе инициализации, следует использовать отдельный конфигурационный файл. Изменим логику таким образом, чтобы для команды login использовался ID контейнера, а не его имя.

Доработаем плейбук создания платформы. Добавим задачи поиска ID, а также генерации конфига в конец плейбука:

src/molecule_custom_docker/playbooks/create.yml

- 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: 0600

Теперь при выполнении команды molecule create будет генерироваться файл ~/.cache/molecule///instance_config.yml, в котором есть список словарей, содержащих информацию о созданных платформах.

~/.cache/molecule/hello_world/test_c/instance_config.yml

- {ID: e9ec39e81e4f85cff47f6ace3cbaf409dbe7846db307241d94a72fcdec89d108, image: 'docker.io/library/python:3.10-slim-buster, instance: molecule-hello_world, tag: 3.10-slim-buster}

Значение для ключа ID вычисляется в ходе работы Ansible-плейбука и недоступно для использования внутри Python-кода. Путь до файла instance_config.yml доступен в свойстве Driver._config.driver.instance_config.

Доработаем драйвер, чтобы при molecule login использовался ID контейнера:

src/molecule_custom_docker/driver.py

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 или какой-либо библиотеки Python. Так было у нас. Следите за тем, что импортируется, запускается и загружается. Внутри драйвера используется библиотека Python? Укажите ее в зависимостях к пакету. Используются Ansible-модули, которые начинаются не с ansible.builtin? Добавьте коллекцию в required_collections.

  • Лучше использовать временные директории. Вместо того чтобы формировать шаблоны для виртуальной машины через lookup-модуль, мы сохраняем сгенерированный файл во временную директорию. Это не раз спасало нас в ходе разработки и отладки новых фич для плагина. 

  • README.md спасет вас от потока вопросов в чатах и почте. Например, мы с командой храним в стартовой документации информацию о том, как установить драйвер, какие пакеты нужны для корректной работы, как протестировать роль.

Спасибо за внимание! Исходный код для molecule_custom_docker вы найдете в репозитории на GitHub.

Тексты, которые могут вас заинтересовать:

→ Как ограничить количество выполняющихся задач в Jenkins при вызове parallel: сравниваем решения

→ История печатных плат: от Эйслера до наших дней

→ Простые правила, которые помогают писать на Go без побочных эффектов

© Habrahabr.ru