Skip to content

Commit 264cb90

Browse files
decentral1sessbarnea
authored andcommitted
Add experimental --parallel functionality (#2137)
Working towards closing #1702.
1 parent eb6666c commit 264cb90

File tree

14 files changed

+180
-10
lines changed

14 files changed

+180
-10
lines changed

CHANGELOG.rst

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ History
55
Unreleased
66
==========
77

8+
* Add the `--parallel` flag to experimentally allow molecule to be run in parallel.
89
* `dependency` step is now run by default before any playbook sequence step, including
910
`create` and `destroy`. This allows the use of roles in all sequence step playbooks.
1011
* Removed validation regex for docker registry passwords, all ``string`` values are now valid.

docs/examples.rst

+38
Original file line numberDiff line numberDiff line change
@@ -279,3 +279,41 @@ lives in a shared location and ``molecule.yml`` is points to the shared tests.
279279
directory: ../resources/tests/
280280
lint:
281281
name: flake8
282+
283+
.. _parallel-usage-example:
284+
285+
Running Molecule processes in parallel mode
286+
===========================================
287+
288+
.. important::
289+
290+
This functionality should be considered experimental. It is part of ongoing
291+
work towards enabling parallelizable functionality across all moving parts
292+
in the execution of the Molecule feature set.
293+
294+
.. note::
295+
296+
Only the following sequences support parallelizable functionality:
297+
298+
* ``check_sequence``: ``molecule check --parallel``
299+
* ``destroy_sequence``: ``molecule destroy --parallel``
300+
* ``test_sequence``: ``molecule test --parallel``
301+
302+
It is currently only available for use with the Docker driver.
303+
304+
It is possible to run Molecule processes in parallel using another tool to
305+
orchestrate the parallelization (such as `GNU Parallel`_ or `Pytest`_).
306+
307+
When Molecule receives the ``--parallel`` flag it will generate a `UUID`_ for
308+
the duration of the testing sequence and will use that unique identifier to
309+
cache the run-time state for that process. The parallel Molecule processes
310+
cached state and created instances will therefore not interfere with each
311+
other.
312+
313+
Molecule uses a new and separate caching folder for this in the
314+
``$HOME/.cache/molecule_parallel`` location. Molecule exposes a new environment
315+
variable ``MOLECULE_PARALLEL`` which can enable this functionality.
316+
317+
.. _GNU Parallel: https://www.gnu.org/software/parallel/
318+
.. _Pytest: https://docs.pytest.org/en/latest/
319+
.. _UUID: https://en.wikipedia.org/wiki/Universally_unique_identifier

docs/faq.rst

+6
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,9 @@ Are there similar tools to Molecule?
7878
.. _`ansible-test`: https://github.com/nylas/ansible-test
7979
.. _`abandoned`: https://github.com/nylas/ansible-test/issues/14
8080
.. _`RoleSpec`: https://github.com/nickjj/rolespec
81+
82+
83+
Can I run Molecule processes in parallel?
84+
=========================================
85+
86+
Please see :ref:`parallel-usage-example` for usage.

molecule/command/base.py

+5
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ def execute_cmdline_scenarios(scenario_name,
110110
execute_subcommand(scenario.config, 'destroy')
111111
# always prune ephemeral dir if destroying on failure
112112
scenario.prune()
113+
if scenario.config.is_parallel:
114+
scenario._remove_scenario_state_directory()
113115
util.sysexit()
114116
else:
115117
raise
@@ -144,6 +146,9 @@ def execute_scenario(scenario):
144146
if 'destroy' in scenario.sequence:
145147
scenario.prune()
146148

149+
if scenario.config.is_parallel:
150+
scenario._remove_scenario_state_directory()
151+
147152

148153
def get_configs(args, command_args, ansible_args=()):
149154
"""

molecule/command/check.py

+18-1
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,15 @@
1818
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
1919
# DEALINGS IN THE SOFTWARE.
2020

21+
import os
2122
import click
2223

2324
from molecule import logger
2425
from molecule.command import base
26+
from molecule import util
2527

2628
LOG = logger.get_logger(__name__)
29+
MOLECULE_PARALLEL = os.environ.get('MOLECULE_PARALLEL', False)
2730

2831

2932
class Check(base.Base):
@@ -58,6 +61,12 @@ class Check(base.Base):
5861
5962
Load an env file to read variables from when rendering
6063
molecule.yml.
64+
65+
.. program:: molecule --parallel check
66+
67+
.. option:: molecule --parallel check
68+
69+
Run in parallelizable mode.
6170
"""
6271

6372
def execute(self):
@@ -79,15 +88,23 @@ def execute(self):
7988
default=base.MOLECULE_DEFAULT_SCENARIO_NAME,
8089
help='Name of the scenario to target. ({})'.format(
8190
base.MOLECULE_DEFAULT_SCENARIO_NAME))
82-
def check(ctx, scenario_name): # pragma: no cover
91+
@click.option(
92+
'--parallel/--no-parallel',
93+
default=MOLECULE_PARALLEL,
94+
help='Enable or disable parallel mode. Default is disabled.')
95+
def check(ctx, scenario_name, parallel): # pragma: no cover
8396
"""
8497
Use the provisioner to perform a Dry-Run (destroy, dependency, create,
8598
prepare, converge).
8699
"""
87100
args = ctx.obj.get('args')
88101
subcommand = base._get_subcommand(__name__)
89102
command_args = {
103+
'parallel': parallel,
90104
'subcommand': subcommand,
91105
}
92106

107+
if parallel:
108+
util.validate_parallel_cmd_args(command_args)
109+
93110
base.execute_cmdline_scenarios(scenario_name, args, command_args)

molecule/command/destroy.py

+20-2
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,16 @@
1818
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
1919
# DEALINGS IN THE SOFTWARE.
2020

21+
import os
2122
import click
2223

2324
from molecule import config
2425
from molecule import logger
2526
from molecule.command import base
27+
from molecule import util
2628

2729
LOG = logger.get_logger(__name__)
30+
MOLECULE_PARALLEL = os.environ.get('MOLECULE_PARALLEL', False)
2831

2932

3033
class Destroy(base.Base):
@@ -71,6 +74,12 @@ class Destroy(base.Base):
7174
7275
Load an env file to read variables from when rendering
7376
molecule.yml.
77+
78+
.. program:: molecule --parallel destroy
79+
80+
.. option:: molecule --parallel destroy
81+
82+
Run in parallelizable mode.
7483
"""
7584

7685
def execute(self):
@@ -112,18 +121,27 @@ def execute(self):
112121
@click.option(
113122
'--all/--no-all',
114123
'__all',
115-
default=False,
124+
default=MOLECULE_PARALLEL,
116125
help='Destroy all scenarios. Default is False.')
117-
def destroy(ctx, scenario_name, driver_name, __all): # pragma: no cover
126+
@click.option(
127+
'--parallel/--no-parallel',
128+
default=False,
129+
help='Enable or disable parallel mode. Default is disabled.')
130+
def destroy(ctx, scenario_name, driver_name, __all,
131+
parallel): # pragma: no cover
118132
""" Use the provisioner to destroy the instances. """
119133
args = ctx.obj.get('args')
120134
subcommand = base._get_subcommand(__name__)
121135
command_args = {
136+
'parallel': parallel,
122137
'subcommand': subcommand,
123138
'driver_name': driver_name,
124139
}
125140

126141
if __all:
127142
scenario_name = None
128143

144+
if parallel:
145+
util.validate_parallel_cmd_args(command_args)
146+
129147
base.execute_cmdline_scenarios(scenario_name, args, command_args)

molecule/command/test.py

+19-1
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,16 @@
1818
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
1919
# DEALINGS IN THE SOFTWARE.
2020

21+
import os
2122
import click
2223

2324
from molecule import config
2425
from molecule import logger
2526
from molecule.command import base
27+
from molecule import util
2628

2729
LOG = logger.get_logger(__name__)
30+
MOLECULE_PARALLEL = os.environ.get('MOLECULE_PARALLEL', False)
2831

2932

3033
class Test(base.Base):
@@ -71,6 +74,12 @@ class Test(base.Base):
7174
7275
Load an env file to read variables from when rendering
7376
molecule.yml.
77+
78+
.. program:: molecule --parallel test
79+
80+
.. option:: molecule --parallel test
81+
82+
Run in parallelizable mode.
7483
"""
7584

7685
def execute(self):
@@ -106,7 +115,12 @@ def execute(self):
106115
default='always',
107116
help=('The destroy strategy used at the conclusion of a '
108117
'Molecule run (always).'))
109-
def test(ctx, scenario_name, driver_name, __all, destroy): # pragma: no cover
118+
@click.option(
119+
'--parallel/--no-parallel',
120+
default=MOLECULE_PARALLEL,
121+
help='Enable or disable parallel mode. Default is disabled.')
122+
def test(ctx, scenario_name, driver_name, __all, destroy,
123+
parallel): # pragma: no cover
110124
"""
111125
Test (lint, cleanup, destroy, dependency, syntax, create, prepare,
112126
converge, idempotence, side_effect, verify, cleanup, destroy).
@@ -115,6 +129,7 @@ def test(ctx, scenario_name, driver_name, __all, destroy): # pragma: no cover
115129
args = ctx.obj.get('args')
116130
subcommand = base._get_subcommand(__name__)
117131
command_args = {
132+
'parallel': parallel,
118133
'destroy': destroy,
119134
'subcommand': subcommand,
120135
'driver_name': driver_name,
@@ -123,4 +138,7 @@ def test(ctx, scenario_name, driver_name, __all, destroy): # pragma: no cover
123138
if __all:
124139
scenario_name = None
125140

141+
if parallel:
142+
util.validate_parallel_cmd_args(command_args)
143+
126144
base.execute_cmdline_scenarios(scenario_name, args, command_args)

molecule/config.py

+13-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
1919
# DEALINGS IN THE SOFTWARE.
2020

21+
from uuid import uuid4
2122
import os
2223

2324
import anyconfig
@@ -109,11 +110,16 @@ def __init__(self,
109110
self.ansible_args = ansible_args
110111
self.config = self._get_config()
111112
self._action = None
113+
self._run_uuid = str(uuid4())
112114

113115
def after_init(self):
114116
self.config = self._reget_config()
115117
self._validate()
116118

119+
@property
120+
def is_parallel(self):
121+
return self.command_args.get('parallel', False)
122+
117123
@property
118124
def debug(self):
119125
return self.args.get('debug', MOLECULE_DEBUG)
@@ -138,6 +144,10 @@ def action(self, value):
138144
def project_directory(self):
139145
return os.getcwd()
140146

147+
@property
148+
def cache_directory(self):
149+
return 'molecule_parallel' if self.is_parallel else 'molecule'
150+
141151
@property
142152
def molecule_directory(self):
143153
return molecule_directory(self.project_directory)
@@ -198,6 +208,7 @@ def env(self):
198208
'MOLECULE_DEBUG': str(self.debug),
199209
'MOLECULE_FILE': self.molecule_file,
200210
'MOLECULE_ENV_FILE': self.env_file,
211+
'MOLECULE_STATE_FILE': self.state.state_file,
201212
'MOLECULE_INVENTORY_FILE': self.provisioner.inventory_file,
202213
'MOLECULE_EPHEMERAL_DIRECTORY': self.scenario.ephemeral_directory,
203214
'MOLECULE_SCENARIO_DIRECTORY': self.scenario.directory,
@@ -224,7 +235,8 @@ def lint(self):
224235
@property
225236
@util.memoize
226237
def platforms(self):
227-
return platforms.Platforms(self)
238+
return platforms.Platforms(
239+
self, parallelize_platforms=self.is_parallel)
228240

229241
@property
230242
@util.memoize

molecule/platforms.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
# DEALINGS IN THE SOFTWARE.
2020

2121
from molecule import logger
22+
from molecule import util
2223

2324
LOG = logger.get_logger(__name__)
2425

@@ -65,13 +66,16 @@ class Platforms(object):
6566
- child_group1
6667
"""
6768

68-
def __init__(self, config):
69+
def __init__(self, config, parallelize_platforms=False):
6970
"""
7071
Initialize a new platform class and returns None.
7172
7273
:param config: An instance of a Molecule config.
7374
:return: None
7475
"""
76+
if parallelize_platforms:
77+
config.config['platforms'] = util._parallelize_platforms(
78+
config.config, config._run_uuid)
7579
self._config = config
7680

7781
@property

molecule/provisioner/ansible/plugins/filters/molecule_core.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ def from_yaml(data):
4343
i = interpolation.Interpolator(interpolation.TemplateWithDefaults, env)
4444
interpolated_data = i.interpolate(data)
4545

46-
return util.safe_load(interpolated_data)
46+
loaded_data = util.safe_load(interpolated_data)
47+
loaded_data = _parallelize_config(loaded_data)
48+
return loaded_data
4749

4850

4951
def to_yaml(data):
@@ -65,6 +67,14 @@ def get_docker_networks(data):
6567
return network_list
6668

6769

70+
def _parallelize_config(data):
71+
state = util.safe_load_file(os.environ['MOLECULE_STATE_FILE'])
72+
if state['is_parallel']:
73+
data['platforms'] = util._parallelize_platforms(
74+
data, state['run_uuid'])
75+
return data
76+
77+
6878
class FilterModule(object):
6979
""" Core Molecule filter plugins. """
7080

molecule/scenario.py

+17-3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
1919
# DEALINGS IN THE SOFTWARE.
2020

21+
import shutil
2122
import os
2223
import fnmatch
2324
try:
@@ -98,6 +99,14 @@ def __init__(self, config):
9899
self.config = config
99100
self._setup()
100101

102+
def _remove_scenario_state_directory(self):
103+
"""Remove scenario cached disk stored state.
104+
105+
:return: None
106+
"""
107+
LOG.info('Removing scenario state directory from cache')
108+
shutil.rmtree(Path(self.ephemeral_directory).parent)
109+
101110
def prune(self):
102111
"""
103112
Prune the scenario ephemeral directory files and returns None.
@@ -137,9 +146,14 @@ def directory(self):
137146
@property
138147
def ephemeral_directory(self):
139148
project_directory = os.path.basename(self.config.project_directory)
140-
scenario_name = self.name
141-
project_scenario_directory = os.path.join(
142-
'molecule', project_directory, scenario_name)
149+
150+
if self.config.is_parallel:
151+
project_directory = '{}-{}'.format(project_directory,
152+
self.config._run_uuid)
153+
154+
project_scenario_directory = os.path.join(self.config.cache_directory,
155+
project_directory, self.name)
156+
143157
path = ephemeral_directory(project_scenario_directory)
144158

145159
return ephemeral_directory(path)

0 commit comments

Comments
 (0)