github.com/muhammedhassanm/blockchain@v0.0.0-20200120143007-697261defd4d/sawtooth-supply-chain-master/bin/run_docker_test (about) 1 #!/usr/bin/env python3 2 # 3 # Copyright 2017 Intel Corporation 4 # 5 # Licensed under the Apache License, Version 2.0 (the "License"); 6 # you may not use this file except in compliance with the License. 7 # You may obtain a copy of the License at 8 # 9 # http://www.apache.org/licenses/LICENSE-2.0 10 # 11 # Unless required by applicable law or agreed to in writing, software 12 # distributed under the License is distributed on an "AS IS" BASIS, 13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 # See the License for the specific language governing permissions and 15 # limitations under the License. 16 17 import subprocess 18 import os 19 import sys 20 import argparse 21 import time 22 import traceback 23 import logging 24 import yaml 25 26 27 logging.basicConfig(level=logging.DEBUG) 28 LOGGER = logging.getLogger(__name__) 29 30 31 DEFAULT_TIMEOUT = 600 32 COMPOSE_DOWN_RETRY_TIMEOUT = 60 33 DOCKER_PS_TIMEOUT=30 34 35 36 class RunDockerTestError(BaseException): 37 pass 38 39 40 class Timer: 41 def __init__(self, duration): 42 self._duration = duration 43 self._start = 0 44 45 def start(self): 46 self._start = time.time() 47 48 def remaining(self): 49 elapsed = time.time() - self._start 50 return max(self._duration - elapsed, 0) 51 52 53 def main(): 54 args = parse_args() 55 56 # Search for compose file passed 57 compose_file = _get_compose_file(args.compose_file) 58 test_service = _get_test_service(compose_file) 59 60 # Load isolation id if it is set and validate 61 isolation_id = _get_isolation_id() 62 _setup_environ(isolation_id) 63 64 # Construct commands 65 compose = [ 66 'docker-compose', 67 '-p', isolation_id, 68 '-f', compose_file 69 ] 70 71 compose_up = compose + [ 72 'up', '--abort-on-container-exit' 73 ] 74 75 compose_down = compose + ['down', '--remove-orphans'] 76 77 compose_kill = compose + ['kill'] 78 79 scrape = [ 80 'docker', 'ps', '-a', 81 '--format={{.Names}},{{.Image}},{{.Label "install-type"}}', 82 ] 83 84 inspect = [ 85 'docker', 'inspect', 86 '-f', "{{.State.ExitCode}}", 87 "{}_{}_1".format( 88 isolation_id, 89 test_service) 90 ] 91 92 compose_dict = load_compose_file(compose_file) 93 _validate_compose_dict(compose_dict, test_service, compose_file) 94 95 if not args.clean: 96 _check_for_existing_containers(compose_file, compose_dict, isolation_id) 97 98 for service in compose_dict['services']: 99 scrape += [ 100 '--filter', 'name={}_{}_1'.format(isolation_id, service), 101 ] 102 103 timer = Timer(args.timeout) 104 105 106 # Run tests 107 try: 108 if not args.clean: 109 exit_status = 0 110 timer.start() 111 112 LOGGER.info("Bringing up with %s", str(compose_up)) 113 try: 114 # 1. Run the tests 115 subprocess.run(compose_up, check=True, timeout=timer.remaining()) 116 117 except FileNotFoundError as err: 118 LOGGER.error("Bad docker-compose up command") 119 LOGGER.exception(err) 120 exit(1) 121 122 except subprocess.CalledProcessError as err: 123 LOGGER.error("Failed to start test.") 124 LOGGER.exception(err) 125 exit_status = 1 126 127 except subprocess.TimeoutExpired as err: 128 LOGGER.error("Test timed out.") 129 LOGGER.exception(err) 130 exit_status = 1 131 132 if exit_status == 0: 133 LOGGER.info("Getting result with: %s", str(inspect)) 134 try: 135 # 2. Get the exit code of the test container 136 exit_status = subprocess.run( 137 inspect, stdout=subprocess.PIPE, 138 timeout=timer.remaining(), check=True 139 ).stdout.decode().strip() 140 141 except FileNotFoundError as err: 142 LOGGER.error("Bad docker inspect or ps command") 143 LOGGER.exception(err) 144 exit_status = 1 145 146 except subprocess.CalledProcessError as err: 147 LOGGER.error("Failed to retrieve exit status of test.") 148 LOGGER.exception(err) 149 exit_status = 1 150 151 except subprocess.TimeoutExpired as err: 152 LOGGER.error("Retrieving exit status timed out.") 153 LOGGER.exception(err) 154 exit_status = 1 155 156 try: 157 info = subprocess.run( 158 scrape, stdout=subprocess.PIPE, 159 timeout=timer.remaining(), check=True 160 ).stdout.decode().strip() 161 for line in info.split('\n'): 162 container, image, install_type = line.split(',') 163 LOGGER.info( 164 "Container {} ran image {} with install-type {}".format( 165 container, image, install_type 166 ) 167 ) 168 169 except BaseException: 170 LOGGER.error("Could not gather information about image used.") 171 172 else: # cleaning 173 exit_status = 0 174 175 LOGGER.info("Shutting down with: %s", str(compose_down)) 176 shutdown_success = False 177 for i in range(2): 178 if shutdown_success == False: 179 180 # Always give compose down time to cleanup 181 timeout = max(timer.remaining(), COMPOSE_DOWN_RETRY_TIMEOUT) 182 try: 183 # 3. Cleanup after the test 184 subprocess.run(compose_down, check=True, timeout=timeout) 185 shutdown_success = True 186 187 except FileNotFoundError as err: 188 LOGGER.error("Bad docker-compose down command.\n") 189 LOGGER.exception(err) 190 191 except subprocess.CalledProcessError as err: 192 LOGGER.error("Failed to cleanup after test.") 193 LOGGER.exception(err) 194 195 except subprocess.TimeoutExpired as err: 196 LOGGER.error("Shutting down the test timed out.") 197 LOGGER.exception(err) 198 199 if not shutdown_success: 200 LOGGER.critical( 201 "There are residual containers on the host that need to be" 202 " cleaned up! Do `docker ps -a` and `docker newtork list` to" 203 " see what was left behind or use `run_docker_test --clean`!" 204 ) 205 206 exit(int(exit_status)) 207 208 except KeyboardInterrupt: 209 subprocess.run(compose_down, check=True, 210 timeout=COMPOSE_DOWN_RETRY_TIMEOUT) 211 exit(1) 212 213 214 def load_compose_file(compose_file): 215 216 try: 217 with open(compose_file) as fd: 218 contents = fd.read() 219 compose = yaml.load(contents) 220 return compose 221 222 except OSError: 223 raise RunDockerTestError( 224 "Docker compose file '{}' could not be opened. Make sure it " 225 "exists and is readable.".format(compose_file)) 226 227 228 def parse_args(): 229 parser = argparse.ArgumentParser() 230 231 parser.add_argument( 232 "compose_file", 233 help="docker-compose.yaml file that contains the test") 234 235 parser.add_argument( 236 "-c", "--clean", 237 help="don't run the test, just cleanup a previous run", 238 action='store_true', 239 default=False) 240 241 parser.add_argument( 242 "-n", "--no-build", 243 help="don't build docker images", 244 action='store_true', 245 default=False) 246 247 parser.add_argument( 248 "-t", "--timeout", 249 help="how long to wait before timing out", 250 type=int, 251 default=DEFAULT_TIMEOUT) 252 253 return parser.parse_args() 254 255 256 def _get_test_service(compose_file): 257 return os.path.basename(compose_file).replace('.yaml', '').replace('_', '-') 258 259 260 def _validate_compose_dict(compose_dict, test_service, compose_file): 261 if test_service not in compose_dict['services']: 262 raise RunDockerTestError( 263 "Test service '{}' does not exist in compose file: '{}'".format( 264 test_service, compose_file)) 265 266 267 def _check_for_existing_containers(compose_file, compose_dict, isolation_id): 268 containers = _get_existing_containers() 269 for service in compose_dict['services'].keys(): 270 container_name_to_create = "{}_{}_1".format(isolation_id, service) 271 for existing_container_name in containers: 272 if container_name_to_create == existing_container_name: 273 raise RunDockerTestError( 274 "The container '{}' which would be created by this test" 275 " already exists!\nDo:\n`run_docker_test --clean {}`\nto" 276 " remove the container, or use docker manually.".format( 277 container_name_to_create, compose_file 278 ) 279 ) 280 281 def _check_for_existing_network(isolation_id): 282 networks = _get_existing_networks() 283 network_to_create = '{}_default'.format(isolation_id) 284 if network_to_create in networks: 285 raise RunDockerTestError( 286 "The network '{}' which would be created by this test already" 287 " exists!\nDo:\n`run_docker_test --clean {}`\nto remove the" 288 " network, or use docker manually.".format( 289 network_to_create, compose_file 290 ) 291 ) 292 293 294 def _get_existing_containers(): 295 cmd = ['docker', 'ps', '-a', '--format={{.Names}}'] 296 success = False 297 try: 298 containers = subprocess.run( 299 cmd, stdout=subprocess.PIPE, check=True, timeout=DOCKER_PS_TIMEOUT 300 ).stdout.decode().strip().split('\n') 301 success = True 302 303 except FileNotFoundError as err: 304 LOGGER.error("Bad docker ps command") 305 LOGGER.exception(err) 306 307 except subprocess.CalledProcessError as err: 308 LOGGER.error("Failed to retrieve exit status of test.") 309 LOGGER.exception(err) 310 311 except subprocess.TimeoutExpired as err: 312 LOGGER.error("Retrieving exit status timed out.") 313 LOGGER.exception(err) 314 315 if not success: 316 raise RunDockerTestError("Failed to get list of docker containers.") 317 318 return containers 319 320 321 def _get_existing_networks(): 322 cmd = ['docker', 'network', 'ls', '--format={{.Name}}'] 323 success = False 324 325 try: 326 networks = subprocess.run( 327 cmd, stdout=subprocess.PIPE, check=True, timeout=DOCKER_PS_TIMEOUT 328 ).stdout.decode().strip().split('\n') 329 success = True 330 331 except FileNotFoundError as err: 332 LOGGER.error("Bad docker network ls command") 333 LOGGER.exception(err) 334 335 except subprocess.CalledProcessError as err: 336 LOGGER.error("Failed to retrieve network list.") 337 LOGGER.exception(err) 338 339 except subprocess.TimeoutExpired as err: 340 LOGGER.error("Retrieving network list timed out.") 341 LOGGER.exception(err) 342 343 if not success: 344 raise RunDockerTestError("Failed to get list of docker networks.") 345 346 return networks 347 348 349 def _get_isolation_id(): 350 if 'ISOLATION_ID' in os.environ: 351 isolation_id = os.environ['ISOLATION_ID'] 352 else: 353 isolation_id = 'latest' 354 355 if not isolation_id.isalnum(): 356 raise RunDockerTestError("ISOLATION_ID must be alphanumeric") 357 358 return isolation_id 359 360 361 def _setup_environ(isolation_id): 362 os.environ['ISOLATION_ID'] = isolation_id 363 os.environ['SAWTOOTH_CORE'] = os.path.dirname( 364 os.path.dirname( 365 os.path.realpath(__file__) 366 ) 367 ) 368 print(os.environ['SAWTOOTH_CORE']) 369 370 371 def _get_compose_dir(): 372 return os.path.join( 373 os.path.dirname(os.path.dirname(os.path.realpath(__file__))), 374 "integration", "sawtooth_integration", "docker" 375 ) 376 377 378 def _get_compose_file(compose_file): 379 # Try "as given" 380 if not os.path.exists(compose_file): 381 # If not, try in integration docker directory 382 compose_file = os.path.join(_get_compose_dir(), compose_file) 383 if not os.path.exists(compose_file): 384 # If not, try appending .yaml 385 compose_file = os.path.join( 386 _get_compose_dir(), 387 "{}.yaml".format(compose_file) 388 ) 389 if not os.path.exists(compose_file): 390 raise RunDockerTestError( 391 "Docker compose file '{}' does not exist.".format(compose_file)) 392 393 return compose_file 394 395 396 if __name__ == "__main__": 397 try: 398 main() 399 400 except RunDockerTestError as err: 401 LOGGER.error(err) 402 exit(1)