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