1#  Copyright (c) 2015-2018 Cisco Systems, Inc.
2#
3#  Permission is hereby granted, free of charge, to any person obtaining a copy
4#  of this software and associated documentation files (the "Software"), to
5#  deal in the Software without restriction, including without limitation the
6#  rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7#  sell copies of the Software, and to permit persons to whom the Software is
8#  furnished to do so, subject to the following conditions:
9#
10#  The above copyright notice and this permission notice shall be included in
11#  all copies or substantial portions of the Software.
12#
13#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14#  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15#  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16#  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17#  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18#  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19#  DEALINGS IN THE SOFTWARE.
20
21import shutil
22import os
23import fnmatch
24
25try:
26    from pathlib import Path
27except ImportError:
28    from pathlib2 import Path
29
30from molecule import logger
31from molecule import scenarios
32from molecule import util
33
34LOG = logger.get_logger(__name__)
35
36
37class Scenario(object):
38    """
39    A scenario allows Molecule test a role in a particular way, this is a
40    fundamental change from Molecule v1.
41
42    A scenario is a self-contained directory containing everything necessary
43    for testing the role in a particular way.  The default scenario is named
44    ``default``, and every role should contain a default scenario.
45
46    Unless mentioned explicitly, the scenario name will be the directory name
47    hosting the files.
48
49    Any option set in this section will override the defaults.
50
51    .. code-block:: yaml
52
53        scenario:
54          name: default  # optional
55          create_sequence:
56            - dependency
57            - create
58            - prepare
59          check_sequence:
60            - dependency
61            - cleanup
62            - destroy
63            - create
64            - prepare
65            - converge
66            - check
67            - destroy
68          converge_sequence:
69            - dependency
70            - create
71            - prepare
72            - converge
73          destroy_sequence:
74            - dependency
75            - cleanup
76            - destroy
77          test_sequence:
78            - lint
79            - dependency
80            - cleanup
81            - destroy
82            - syntax
83            - create
84            - prepare
85            - converge
86            - idempotence
87            - side_effect
88            - verify
89            - cleanup
90            - destroy
91    """  # noqa
92
93    def __init__(self, config):
94        """
95        Initialize a new scenario class and returns None.
96
97        :param config: An instance of a Molecule config.
98        :return: None
99        """
100        self.config = config
101        self._setup()
102
103    def _remove_scenario_state_directory(self):
104        """Remove scenario cached disk stored state.
105
106        :return: None
107        """
108        directory = str(Path(self.ephemeral_directory).parent)
109        LOG.info('Removing {}'.format(directory))
110        shutil.rmtree(directory)
111
112    def prune(self):
113        """
114        Prune the scenario ephemeral directory files and returns None.
115
116        "safe files" will not be pruned, including the ansible configuration
117        and inventory used by this scenario, the scenario state file, and
118        files declared as "safe_files" in the ``driver`` configuration
119        declared in ``molecule.yml``.
120
121        :return: None
122        """
123        LOG.info('Pruning extra files from scenario ephemeral directory')
124        safe_files = [
125            self.config.provisioner.config_file,
126            self.config.provisioner.inventory_file,
127            self.config.state.state_file,
128        ] + self.config.driver.safe_files
129        files = util.os_walk(self.ephemeral_directory, '*')
130        for f in files:
131            if not any(sf for sf in safe_files if fnmatch.fnmatch(f, sf)):
132                os.remove(f)
133
134        # Remove empty directories.
135        for dirpath, dirs, files in os.walk(self.ephemeral_directory, topdown=False):
136            if not dirs and not files:
137                os.removedirs(dirpath)
138
139    @property
140    def name(self):
141        return self.config.config['scenario']['name']
142
143    @property
144    def directory(self):
145        return os.path.dirname(self.config.molecule_file)
146
147    @property
148    def ephemeral_directory(self):
149        _ephemeral_directory = os.getenv('MOLECULE_EPHEMERAL_DIRECTORY')
150        if _ephemeral_directory:
151            return _ephemeral_directory
152
153        project_directory = os.path.basename(self.config.project_directory)
154
155        if self.config.is_parallel:
156            project_directory = '{}-{}'.format(project_directory, self.config._run_uuid)
157
158        project_scenario_directory = os.path.join(
159            self.config.cache_directory, project_directory, self.name
160        )
161
162        path = ephemeral_directory(project_scenario_directory)
163
164        return ephemeral_directory(path)
165
166    @property
167    def inventory_directory(self):
168        return os.path.join(self.ephemeral_directory, "inventory")
169
170    @property
171    def check_sequence(self):
172        return self.config.config['scenario']['check_sequence']
173
174    @property
175    def cleanup_sequence(self):
176        return self.config.config['scenario']['cleanup_sequence']
177
178    @property
179    def converge_sequence(self):
180        return self.config.config['scenario']['converge_sequence']
181
182    @property
183    def create_sequence(self):
184        return self.config.config['scenario']['create_sequence']
185
186    @property
187    def dependency_sequence(self):
188        return ['dependency']
189
190    @property
191    def destroy_sequence(self):
192        return self.config.config['scenario']['destroy_sequence']
193
194    @property
195    def idempotence_sequence(self):
196        return ['idempotence']
197
198    @property
199    def lint_sequence(self):
200        return ['lint']
201
202    @property
203    def prepare_sequence(self):
204        return ['prepare']
205
206    @property
207    def side_effect_sequence(self):
208        return ['side_effect']
209
210    @property
211    def syntax_sequence(self):
212        return ['syntax']
213
214    @property
215    def test_sequence(self):
216        return self.config.config['scenario']['test_sequence']
217
218    @property
219    def verify_sequence(self):
220        return ['verify']
221
222    @property
223    def sequence(self):
224        """
225        Select the sequence based on scenario and subcommand of the provided
226        scenario object and returns a list.
227
228        :param scenario: A scenario object.
229        :param skipped: An optional bool to include skipped scenarios.
230        :return: list
231        """
232        s = scenarios.Scenarios([self.config])
233        matrix = s._get_matrix()
234
235        try:
236            return matrix[self.name][self.config.subcommand]
237        except KeyError:
238            # TODO(retr0h): May change this handling in the future.
239            return []
240
241    def _setup(self):
242        """
243         Prepare the scenario for Molecule and returns None.
244
245         :return: None
246         """
247        if not os.path.isdir(self.inventory_directory):
248            os.makedirs(self.inventory_directory)
249
250
251def ephemeral_directory(path=None):
252    """
253    Returns temporary directory to be used by molecule. Molecule users should
254    not make any assumptions about its location, permissions or its content as
255    this may change in future release.
256    """
257    d = os.getenv('MOLECULE_EPHEMERAL_DIRECTORY')
258    if not d:
259        d = os.getenv("XDG_CACHE_HOME", os.path.expanduser("~/.cache"))
260    d = os.path.abspath(os.path.join(d, path if path else 'molecule'))
261
262    if not os.path.isdir(d):
263        os.umask(0o077)
264        Path(d).mkdir(mode=0o700, parents=True, exist_ok=True)
265
266    return d
267