diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4034f6b06e..ab52a973b1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,7 @@ History Unreleased ========== +* Add the `--parallel` flag to experimentally allow molecule to be run in parallel. * `dependency` step is now run by default before any playbook sequence step, including `create` and `destroy`. This allows the use of roles in all sequence step playbooks. * Removed validation regex for docker registry passwords, all ``string`` values are now valid. diff --git a/docs/examples.rst b/docs/examples.rst index fe5b97a015..608a4b4a9f 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -279,3 +279,50 @@ lives in a shared location and ``molecule.yml`` is points to the shared tests. directory: ../resources/tests/ lint: name: flake8 + +.. _parallel-usage-example: + +Running Molecule processes in parallel mode +=========================================== + +.. important:: + + This functionality should be considered experimental. It is part of ongoing + work towards enabling parallelizable functionality across all moving parts + in the execution of the Molecule feature set. + +.. note:: + + Only the following sequences support parallelizable functionality: + + * ``check_sequence``: ``molecule check --parallel`` + * ``destroy_sequence``: ``molecule destroy --parallel`` + * ``test_sequence``: ``molecule test --parallel`` + + It is currently only available for use with the Docker driver. + +It is possible to run Molecule processes in parallel using another tool to +orchestrate the parallelization (such as `GNU Parallel`_ or `Pytest`_). + +When Molecule receives the ``--parallel`` flag it will generate a `UUID`_ for +the duration of the testing sequence and will use that unique identifier to +cache the run-time state for that process. The parallel Molecule processes +cached state and created instances will therefore not interfere with each +other. + +Molecule uses a new and separate caching folder for this in the +``$HOME/.cache/molecule_parallel`` location. + +Two new variables (``molecule_parallel`` and ``molecule_run_uuid``) and a new +filter (``molecule_parallelize``) are introduced into the playbook context from +the Molecule internals. The API and usage of these variables and filters +**cannot** be relied on. They are experimental and may be purely for internal +use in the coming iterations. + +If you are writing custom playbooks for your ``create.yml`` and ``destroy.yml`` +then it is your responsibility to adapt your playbooks to allow for +``--parallel`` based functionality. + +.. _GNU Parallel: https://www.gnu.org/software/parallel/ +.. _Pytest: https://docs.pytest.org/en/latest/ +.. _UUID: https://en.wikipedia.org/wiki/Universally_unique_identifier diff --git a/docs/faq.rst b/docs/faq.rst index 148a4b8cf6..326400ef2c 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -78,3 +78,9 @@ Are there similar tools to Molecule? .. _`ansible-test`: https://github.com/nylas/ansible-test .. _`abandoned`: https://github.com/nylas/ansible-test/issues/14 .. _`RoleSpec`: https://github.com/nickjj/rolespec + + +Can I run Molecule processes in parallel? +========================================= + +Please see :ref:`parallel-usage-example` for usage. diff --git a/molecule/command/base.py b/molecule/command/base.py index 58d5bdbed1..d4721de9a9 100644 --- a/molecule/command/base.py +++ b/molecule/command/base.py @@ -110,6 +110,8 @@ def execute_cmdline_scenarios(scenario_name, execute_subcommand(scenario.config, 'destroy') # always prune ephemeral dir if destroying on failure scenario.prune() + if scenario.config.is_parallel: + scenario._remove_scenario_state_directory() util.sysexit() else: raise @@ -144,6 +146,9 @@ def execute_scenario(scenario): if 'destroy' in scenario.sequence: scenario.prune() + if scenario.config.is_parallel: + scenario._remove_scenario_state_directory() + def get_configs(args, command_args, ansible_args=()): """ diff --git a/molecule/command/check.py b/molecule/command/check.py index 00288f8f60..0220ee2772 100644 --- a/molecule/command/check.py +++ b/molecule/command/check.py @@ -22,6 +22,7 @@ from molecule import logger from molecule.command import base +from molecule import util LOG = logger.get_logger(__name__) @@ -58,6 +59,12 @@ class Check(base.Base): Load an env file to read variables from when rendering molecule.yml. + + .. program:: molecule --parallel check + + .. option:: molecule --parallel check + + Run in parallelizable mode. """ def execute(self): @@ -79,7 +86,11 @@ def execute(self): default=base.MOLECULE_DEFAULT_SCENARIO_NAME, help='Name of the scenario to target. ({})'.format( base.MOLECULE_DEFAULT_SCENARIO_NAME)) -def check(ctx, scenario_name): # pragma: no cover +@click.option( + '--parallel/--no-parallel', + default=False, + help='Enable or disable parallel mode. Default is disabled.') +def check(ctx, scenario_name, parallel): # pragma: no cover """ Use the provisioner to perform a Dry-Run (destroy, dependency, create, prepare, converge). @@ -87,7 +98,11 @@ def check(ctx, scenario_name): # pragma: no cover args = ctx.obj.get('args') subcommand = base._get_subcommand(__name__) command_args = { + 'parallel': parallel, 'subcommand': subcommand, } + if parallel: + util.validate_parallel_cmd_args(command_args) + base.execute_cmdline_scenarios(scenario_name, args, command_args) diff --git a/molecule/command/destroy.py b/molecule/command/destroy.py index 7492848c7c..8624443286 100644 --- a/molecule/command/destroy.py +++ b/molecule/command/destroy.py @@ -23,6 +23,7 @@ from molecule import config from molecule import logger from molecule.command import base +from molecule import util LOG = logger.get_logger(__name__) @@ -71,6 +72,12 @@ class Destroy(base.Base): Load an env file to read variables from when rendering molecule.yml. + + .. program:: molecule --parallel destroy + + .. option:: molecule --parallel destroy + + Run in parallelizable mode. """ def execute(self): @@ -114,11 +121,17 @@ def execute(self): '__all', default=False, help='Destroy all scenarios. Default is False.') -def destroy(ctx, scenario_name, driver_name, __all): # pragma: no cover +@click.option( + '--parallel/--no-parallel', + default=False, + help='Enable or disable parallel mode. Default is disabled.') +def destroy(ctx, scenario_name, driver_name, __all, + parallel): # pragma: no cover """ Use the provisioner to destroy the instances. """ args = ctx.obj.get('args') subcommand = base._get_subcommand(__name__) command_args = { + 'parallel': parallel, 'subcommand': subcommand, 'driver_name': driver_name, } @@ -126,4 +139,7 @@ def destroy(ctx, scenario_name, driver_name, __all): # pragma: no cover if __all: scenario_name = None + if parallel: + util.validate_parallel_cmd_args(command_args) + base.execute_cmdline_scenarios(scenario_name, args, command_args) diff --git a/molecule/command/test.py b/molecule/command/test.py index 7db0cd8796..ef96fc1b79 100644 --- a/molecule/command/test.py +++ b/molecule/command/test.py @@ -23,6 +23,7 @@ from molecule import config from molecule import logger from molecule.command import base +from molecule import util LOG = logger.get_logger(__name__) @@ -71,6 +72,12 @@ class Test(base.Base): Load an env file to read variables from when rendering molecule.yml. + + .. program:: molecule --parallel test + + .. option:: molecule --parallel test + + Run in parallelizable mode. """ def execute(self): @@ -106,7 +113,12 @@ def execute(self): default='always', help=('The destroy strategy used at the conclusion of a ' 'Molecule run (always).')) -def test(ctx, scenario_name, driver_name, __all, destroy): # pragma: no cover +@click.option( + '--parallel/--no-parallel', + default=False, + help='Enable or disable parallel mode. Default is disabled.') +def test(ctx, scenario_name, driver_name, __all, destroy, + parallel): # pragma: no cover """ Test (lint, cleanup, destroy, dependency, syntax, create, prepare, converge, idempotence, side_effect, verify, cleanup, destroy). @@ -115,6 +127,7 @@ def test(ctx, scenario_name, driver_name, __all, destroy): # pragma: no cover args = ctx.obj.get('args') subcommand = base._get_subcommand(__name__) command_args = { + 'parallel': parallel, 'destroy': destroy, 'subcommand': subcommand, 'driver_name': driver_name, @@ -123,4 +136,7 @@ def test(ctx, scenario_name, driver_name, __all, destroy): # pragma: no cover if __all: scenario_name = None + if parallel: + util.validate_parallel_cmd_args(command_args) + base.execute_cmdline_scenarios(scenario_name, args, command_args) diff --git a/molecule/config.py b/molecule/config.py index baf0fd4653..2d9cded191 100644 --- a/molecule/config.py +++ b/molecule/config.py @@ -18,6 +18,7 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. +from uuid import uuid4 import os import anyconfig @@ -109,11 +110,16 @@ def __init__(self, self.ansible_args = ansible_args self.config = self._get_config() self._action = None + self._run_uuid = str(uuid4()) def after_init(self): self.config = self._reget_config() self._validate() + @property + def is_parallel(self): + return self.command_args.get('parallel', False) + @property def debug(self): return self.args.get('debug', MOLECULE_DEBUG) @@ -138,6 +144,10 @@ def action(self, value): def project_directory(self): return os.getcwd() + @property + def cache_directory(self): + return 'molecule_parallel' if self.is_parallel else 'molecule' + @property def molecule_directory(self): return molecule_directory(self.project_directory) @@ -224,7 +234,8 @@ def lint(self): @property @util.memoize def platforms(self): - return platforms.Platforms(self) + return platforms.Platforms( + self, parallelize_platforms=self.is_parallel) @property @util.memoize diff --git a/molecule/platforms.py b/molecule/platforms.py index ed3a98343c..5e99dc34a6 100644 --- a/molecule/platforms.py +++ b/molecule/platforms.py @@ -65,15 +65,34 @@ class Platforms(object): - child_group1 """ - def __init__(self, config): + def __init__(self, config, parallelize_platforms=False): """ Initialize a new platform class and returns None. :param config: An instance of a Molecule config. :return: None """ + if parallelize_platforms: + config.config['platforms'] = self._parallelize_platforms(config) self._config = config @property def instances(self): return self._config.config['platforms'] + + def _parallelize_platforms(self, config): + """ + Internally parallize the platform instance names. + + :param config: An instance of a Molecule config. + :return: list + """ + + def parallelize(platform): + platform['name'] = '{}-{}'.format(platform['name'], + config._run_uuid) + return platform + + return [ + parallelize(platform) for platform in config.config['platforms'] + ] diff --git a/molecule/provisioner/ansible.py b/molecule/provisioner/ansible.py index 99430b0e04..466e9b792f 100644 --- a/molecule/provisioner/ansible.py +++ b/molecule/provisioner/ansible.py @@ -569,7 +569,11 @@ def inventory(self): "{{ lookup('env', 'MOLECULE_INSTANCE_CONFIG') }}", 'molecule_no_log': "{{ lookup('env', 'MOLECULE_NO_LOG') or not " - "molecule_yml.provisioner.log|default(False) | bool }}" + "molecule_yml.provisioner.log|default(False) | bool }}", + 'molecule_parallel': + self._config.is_parallel, + 'molecule_run_uuid': + self._config._run_uuid, } # All group diff --git a/molecule/provisioner/ansible/playbooks/docker/create.yml b/molecule/provisioner/ansible/playbooks/docker/create.yml index ab5c724d3c..88314f56bd 100644 --- a/molecule/provisioner/ansible/playbooks/docker/create.yml +++ b/molecule/provisioner/ansible/playbooks/docker/create.yml @@ -121,7 +121,7 @@ - name: Create molecule instance(s) docker_container: - name: "{{ item.name }}" + name: "{{ item.name | molecule_parallelize(molecule_parallel, molecule_run_uuid) }}" docker_host: "{{ item.docker_host | default(lookup('env', 'DOCKER_HOST') or 'unix://var/run/docker.sock') }}" cacert_path: "{{ item.cacert_path | default((lookup('env', 'DOCKER_CERT_PATH') + '/ca.pem') if lookup('env', 'DOCKER_CERT_PATH') else omit) }}" cert_path: "{{ item.cert_path | default((lookup('env', 'DOCKER_CERT_PATH') + '/cert.pem') if lookup('env', 'DOCKER_CERT_PATH') else omit) }}" diff --git a/molecule/provisioner/ansible/playbooks/docker/destroy.yml b/molecule/provisioner/ansible/playbooks/docker/destroy.yml index 6ec209b16b..853b31a4af 100644 --- a/molecule/provisioner/ansible/playbooks/docker/destroy.yml +++ b/molecule/provisioner/ansible/playbooks/docker/destroy.yml @@ -7,7 +7,7 @@ tasks: - name: Destroy molecule instance(s) docker_container: - name: "{{ item.name }}" + name: "{{ item.name | molecule_parallelize(molecule_parallel, molecule_run_uuid) }}" docker_host: "{{ item.docker_host | default(lookup('env', 'DOCKER_HOST') or 'unix://var/run/docker.sock') }}" cacert_path: "{{ item.cacert_path | default((lookup('env', 'DOCKER_CERT_PATH') + '/ca.pem') if lookup('env', 'DOCKER_CERT_PATH') else omit) }}" cert_path: "{{ item.cert_path | default((lookup('env', 'DOCKER_CERT_PATH') + '/cert.pem') if lookup('env', 'DOCKER_CERT_PATH') else omit) }}" diff --git a/molecule/provisioner/ansible/plugins/filters/molecule_core.py b/molecule/provisioner/ansible/plugins/filters/molecule_core.py index c99559f625..54fae7124f 100644 --- a/molecule/provisioner/ansible/plugins/filters/molecule_core.py +++ b/molecule/provisioner/ansible/plugins/filters/molecule_core.py @@ -65,6 +65,12 @@ def get_docker_networks(data): return network_list +def parallelize(item, is_parallel, uuid): + if is_parallel: + return '{}-{}'.format(item, uuid) + return item + + class FilterModule(object): """ Core Molecule filter plugins. """ @@ -74,4 +80,5 @@ def filters(self): 'molecule_to_yaml': to_yaml, 'molecule_header': header, 'molecule_get_docker_networks': get_docker_networks, + 'molecule_parallelize': parallelize, } diff --git a/molecule/scenario.py b/molecule/scenario.py index ed17618c0e..a1c4706aa8 100644 --- a/molecule/scenario.py +++ b/molecule/scenario.py @@ -18,6 +18,7 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. +import shutil import os import fnmatch try: @@ -98,6 +99,14 @@ def __init__(self, config): self.config = config self._setup() + def _remove_scenario_state_directory(self): + """Remove scenario cached disk stored state. + + :return: None + """ + LOG.info('Removing scenario state directory from cache') + shutil.rmtree(Path(self.ephemeral_directory).parent) + def prune(self): """ Prune the scenario ephemeral directory files and returns None. @@ -137,9 +146,14 @@ def directory(self): @property def ephemeral_directory(self): project_directory = os.path.basename(self.config.project_directory) - scenario_name = self.name - project_scenario_directory = os.path.join( - 'molecule', project_directory, scenario_name) + + if self.config.is_parallel: + project_directory = '{}-{}'.format(project_directory, + self.config._run_uuid) + + project_scenario_directory = os.path.join(self.config.cache_directory, + project_directory, self.name) + path = ephemeral_directory(project_scenario_directory) return ephemeral_directory(path) diff --git a/molecule/util.py b/molecule/util.py index 34246afd08..f9c842fb0b 100644 --- a/molecule/util.py +++ b/molecule/util.py @@ -311,3 +311,9 @@ def wrapper(*args, **kwargs): return memo[args] return wrapper + + +def validate_parallel_cmd_args(cmd_args): + if cmd_args.get('parallel') and cmd_args.get('destroy') == 'never': + msg = 'Combining "--parallel" and "--destroy=never" is not supported' + sysexit_with_message(msg) diff --git a/test/resources/playbooks/docker/create.yml b/test/resources/playbooks/docker/create.yml index e6a4efe6f4..3a71d7a3de 100644 --- a/test/resources/playbooks/docker/create.yml +++ b/test/resources/playbooks/docker/create.yml @@ -111,7 +111,7 @@ - name: Create molecule instance(s) docker_container: - name: "{{ item.name }}" + name: "{{ item.name | molecule_parallelize(molecule_parallel, molecule_run_uuid) }}" docker_host: "{{ item.docker_host | default(lookup('env', 'DOCKER_HOST') or 'unix://var/run/docker.sock') }}" cacert_path: "{{ item.cacert_path | default((lookup('env', 'DOCKER_CERT_PATH') + '/ca.pem') if lookup('env', 'DOCKER_CERT_PATH') else omit) }}" cert_path: "{{ item.cert_path | default((lookup('env', 'DOCKER_CERT_PATH') + '/cert.pem') if lookup('env', 'DOCKER_CERT_PATH') else omit) }}" diff --git a/test/resources/playbooks/docker/destroy.yml b/test/resources/playbooks/docker/destroy.yml index 213ec61e82..2d2b69945c 100644 --- a/test/resources/playbooks/docker/destroy.yml +++ b/test/resources/playbooks/docker/destroy.yml @@ -7,7 +7,7 @@ tasks: - name: Destroy molecule instance(s) docker_container: - name: "{{ item.name }}" + name: "{{ item.name | molecule_parallelize(molecule_parallel, molecule_run_uuid) }}" docker_host: "{{ item.docker_host | default(lookup('env', 'DOCKER_HOST') or 'unix://var/run/docker.sock') }}" cacert_path: "{{ item.cacert_path | default((lookup('env', 'DOCKER_CERT_PATH') + '/ca.pem') if lookup('env', 'DOCKER_CERT_PATH') else omit) }}" cert_path: "{{ item.cert_path | default((lookup('env', 'DOCKER_CERT_PATH') + '/cert.pem') if lookup('env', 'DOCKER_CERT_PATH') else omit) }}" diff --git a/test/unit/provisioner/test_ansible.py b/test/unit/provisioner/test_ansible.py index 70a77e4fd6..feb711dc05 100644 --- a/test/unit/provisioner/test_ansible.py +++ b/test/unit/provisioner/test_ansible.py @@ -348,8 +348,7 @@ def test_inventory_property(_instance): } }, 'vars': { - 'molecule_file': - "{{ lookup('env', 'MOLECULE_FILE') }}", + 'molecule_file': "{{ lookup('env', 'MOLECULE_FILE') }}", 'molecule_ephemeral_directory': "{{ lookup('env', 'MOLECULE_EPHEMERAL_DIRECTORY') }}", 'molecule_scenario_directory': @@ -361,6 +360,8 @@ def test_inventory_property(_instance): 'molecule_no_log': "{{ lookup('env', 'MOLECULE_NO_LOG') or not " "molecule_yml.provisioner.log|default(False) | bool }}", + 'molecule_parallel': _instance._config.is_parallel, + 'molecule_run_uuid': _instance._config._run_uuid, } }, 'all': { @@ -375,8 +376,7 @@ def test_inventory_property(_instance): } }, 'vars': { - 'molecule_file': - "{{ lookup('env', 'MOLECULE_FILE') }}", + 'molecule_file': "{{ lookup('env', 'MOLECULE_FILE') }}", 'molecule_ephemeral_directory': "{{ lookup('env', 'MOLECULE_EPHEMERAL_DIRECTORY') }}", 'molecule_scenario_directory': @@ -388,6 +388,8 @@ def test_inventory_property(_instance): 'molecule_no_log': "{{ lookup('env', 'MOLECULE_NO_LOG') or not " "molecule_yml.provisioner.log|default(False) | bool }}", + 'molecule_parallel': _instance._config.is_parallel, + 'molecule_run_uuid': _instance._config._run_uuid, } }, 'foo': { @@ -420,8 +422,7 @@ def test_inventory_property(_instance): } }, 'vars': { - 'molecule_file': - "{{ lookup('env', 'MOLECULE_FILE') }}", + 'molecule_file': "{{ lookup('env', 'MOLECULE_FILE') }}", 'molecule_ephemeral_directory': "{{ lookup('env', 'MOLECULE_EPHEMERAL_DIRECTORY') }}", 'molecule_scenario_directory': @@ -433,6 +434,8 @@ def test_inventory_property(_instance): 'molecule_no_log': "{{ lookup('env', 'MOLECULE_NO_LOG') or not " "molecule_yml.provisioner.log|default(False) | bool }}", + 'molecule_parallel': _instance._config.is_parallel, + 'molecule_run_uuid': _instance._config._run_uuid, } }, 'baz': { @@ -453,8 +456,7 @@ def test_inventory_property(_instance): } }, 'vars': { - 'molecule_file': - "{{ lookup('env', 'MOLECULE_FILE') }}", + 'molecule_file': "{{ lookup('env', 'MOLECULE_FILE') }}", 'molecule_ephemeral_directory': "{{ lookup('env', 'MOLECULE_EPHEMERAL_DIRECTORY') }}", 'molecule_scenario_directory': @@ -466,6 +468,8 @@ def test_inventory_property(_instance): 'molecule_no_log': "{{ lookup('env', 'MOLECULE_NO_LOG') or not " "molecule_yml.provisioner.log|default(False) | bool }}", + 'molecule_parallel': _instance._config.is_parallel, + 'molecule_run_uuid': _instance._config._run_uuid, } } } @@ -505,8 +509,7 @@ def test_inventory_property_handles_missing_groups(temp_dir, _instance): } }, 'vars': { - 'molecule_file': - "{{ lookup('env', 'MOLECULE_FILE') }}", + 'molecule_file': "{{ lookup('env', 'MOLECULE_FILE') }}", 'molecule_ephemeral_directory': "{{ lookup('env', 'MOLECULE_EPHEMERAL_DIRECTORY') }}", 'molecule_scenario_directory': @@ -518,6 +521,8 @@ def test_inventory_property_handles_missing_groups(temp_dir, _instance): 'molecule_no_log': "{{ lookup('env', 'MOLECULE_NO_LOG') or not " "molecule_yml.provisioner.log|default(False) | bool }}", + 'molecule_parallel': _instance._config.is_parallel, + 'molecule_run_uuid': _instance._config._run_uuid, } } } @@ -849,8 +854,7 @@ def test_write_inventory(temp_dir, _instance): } }, 'vars': { - 'molecule_file': - "{{ lookup('env', 'MOLECULE_FILE') }}", + 'molecule_file': "{{ lookup('env', 'MOLECULE_FILE') }}", 'molecule_ephemeral_directory': "{{ lookup('env', 'MOLECULE_EPHEMERAL_DIRECTORY') }}", 'molecule_scenario_directory': @@ -862,6 +866,8 @@ def test_write_inventory(temp_dir, _instance): 'molecule_no_log': "{{ lookup('env', 'MOLECULE_NO_LOG') or not " "molecule_yml.provisioner.log|default(False) | bool }}", + 'molecule_parallel': _instance._config.is_parallel, + 'molecule_run_uuid': _instance._config._run_uuid, } }, 'all': { @@ -876,8 +882,7 @@ def test_write_inventory(temp_dir, _instance): } }, 'vars': { - 'molecule_file': - "{{ lookup('env', 'MOLECULE_FILE') }}", + 'molecule_file': "{{ lookup('env', 'MOLECULE_FILE') }}", 'molecule_ephemeral_directory': "{{ lookup('env', 'MOLECULE_EPHEMERAL_DIRECTORY') }}", 'molecule_scenario_directory': @@ -889,6 +894,8 @@ def test_write_inventory(temp_dir, _instance): 'molecule_no_log': "{{ lookup('env', 'MOLECULE_NO_LOG') or not " "molecule_yml.provisioner.log|default(False) | bool }}", + 'molecule_parallel': _instance._config.is_parallel, + 'molecule_run_uuid': _instance._config._run_uuid, } }, 'foo': { @@ -921,8 +928,7 @@ def test_write_inventory(temp_dir, _instance): } }, 'vars': { - 'molecule_file': - "{{ lookup('env', 'MOLECULE_FILE') }}", + 'molecule_file': "{{ lookup('env', 'MOLECULE_FILE') }}", 'molecule_ephemeral_directory': "{{ lookup('env', 'MOLECULE_EPHEMERAL_DIRECTORY') }}", 'molecule_scenario_directory': @@ -934,6 +940,8 @@ def test_write_inventory(temp_dir, _instance): 'molecule_no_log': "{{ lookup('env', 'MOLECULE_NO_LOG') or not " "molecule_yml.provisioner.log|default(False) | bool }}", + 'molecule_parallel': _instance._config.is_parallel, + 'molecule_run_uuid': _instance._config._run_uuid, } }, 'baz': { @@ -954,8 +962,7 @@ def test_write_inventory(temp_dir, _instance): } }, 'vars': { - 'molecule_file': - "{{ lookup('env', 'MOLECULE_FILE') }}", + 'molecule_file': "{{ lookup('env', 'MOLECULE_FILE') }}", 'molecule_ephemeral_directory': "{{ lookup('env', 'MOLECULE_EPHEMERAL_DIRECTORY') }}", 'molecule_scenario_directory': @@ -967,6 +974,8 @@ def test_write_inventory(temp_dir, _instance): 'molecule_no_log': "{{ lookup('env', 'MOLECULE_NO_LOG') or not " "molecule_yml.provisioner.log|default(False) | bool }}", + 'molecule_parallel': _instance._config.is_parallel, + 'molecule_run_uuid': _instance._config._run_uuid, } } }