github.com/muhammedhassanm/blockchain@v0.0.0-20200120143007-697261defd4d/sawtooth-core-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 argparse 20 import time 21 import logging 22 import yaml 23 24 25 logging.basicConfig(level=logging.DEBUG) 26 LOGGER = logging.getLogger(__name__) 27 28 29 DEFAULT_TIMEOUT = 600 30 COMPOSE_DOWN_RETRY_TIMEOUT = 60 31 DOCKER_PS_TIMEOUT = 30 32 33 34 class RunDockerTestError(BaseException): 35 pass 36 37 38 class Timer: 39 def __init__(self, duration): 40 self._duration = duration 41 self._start = 0 42 43 def start(self): 44 self._start = time.time() 45 46 def remaining(self): 47 elapsed = time.time() - self._start 48 return max(self._duration - elapsed, 0) 49 50 51 def main(): 52 args = parse_args() 53 54 # Search for compose file passed 55 compose_file = _get_compose_file(args.compose_file) 56 test_service = _get_test_service(compose_file) 57 58 # Load isolation id if it is set and validate 59 isolation_id = _get_isolation_id() 60 _setup_environ(isolation_id) 61 62 # Construct commands 63 compose = [ 64 'docker-compose', 65 '-p', isolation_id, 66 '-f', compose_file 67 ] 68 69 compose_up = compose + [ 70 'up', '--abort-on-container-exit' 71 ] 72 73 compose_down = compose + ['down', '--remove-orphans'] 74 75 scrape = [ 76 'docker', 'ps', '-a', 77 '--format={{.Names}},{{.Image}},{{.Label "install-type"}}', 78 ] 79 80 inspect = [ 81 'docker', 'inspect', 82 '-f', "{{.State.ExitCode}}", 83 "{}_{}_1".format( 84 isolation_id, 85 test_service) 86 ] 87 88 compose_dict = load_compose_file(compose_file) 89 _validate_compose_dict(compose_dict, test_service, compose_file) 90 91 test_service_image = _get_test_service_image( 92 compose_dict, test_service, compose_file) 93 94 if not args.clean: 95 _check_for_existing_containers( 96 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 # Run tests 106 try: 107 if not args.clean: 108 exit_status = 0 109 110 if not _check_for_existing_image(test_service_image, isolation_id): 111 _build_test_service_image(test_service, compose) 112 113 timer.start() 114 115 test_service_uppercase = test_service.upper() 116 117 LOGGER.info('Starting test %s', test_service_uppercase) 118 119 LOGGER.info("Bringing up with %s", str(compose_up)) 120 121 try: 122 # 1. Run the tests 123 subprocess.run( 124 compose_up, 125 check=True, 126 timeout=timer.remaining()) 127 128 except FileNotFoundError as err: 129 LOGGER.error("Bad docker-compose up command") 130 LOGGER.exception(err) 131 exit(1) 132 133 except subprocess.CalledProcessError as err: 134 LOGGER.error("Test error in %s", test_service_uppercase) 135 LOGGER.exception(err) 136 exit_status = 1 137 138 except subprocess.TimeoutExpired as err: 139 LOGGER.error("Test %s timed out.", test_service_uppercase) 140 LOGGER.exception(err) 141 exit_status = 1 142 143 if exit_status == 0: 144 LOGGER.info("Getting result with: %s", str(inspect)) 145 try: 146 # 2. Get the exit code of the test container 147 exit_status = int(subprocess.run( 148 inspect, stdout=subprocess.PIPE, 149 timeout=timer.remaining(), check=True 150 ).stdout.decode().strip()) 151 152 except FileNotFoundError as err: 153 LOGGER.error("Bad docker inspect or ps command") 154 LOGGER.exception(err) 155 exit_status = 1 156 157 except subprocess.CalledProcessError as err: 158 LOGGER.error("Failed to retrieve exit status of test.") 159 LOGGER.exception(err) 160 exit_status = 1 161 162 except subprocess.TimeoutExpired as err: 163 LOGGER.error("Retrieving exit status timed out.") 164 LOGGER.exception(err) 165 exit_status = 1 166 167 try: 168 info = subprocess.run( 169 scrape, stdout=subprocess.PIPE, 170 timeout=timer.remaining(), check=True 171 ).stdout.decode().strip() 172 173 for line in info.split('\n'): 174 container, image, install_type = line.split(',') 175 LOGGER.info( 176 "Container %s ran image %s with install-type %s", 177 container, image, install_type 178 ) 179 180 except BaseException: 181 LOGGER.error("Could not gather information about image used.") 182 183 else: # cleaning 184 exit_status = 0 185 186 LOGGER.info("Shutting down with: %s", str(compose_down)) 187 188 shutdown_success = False 189 for _ in range(2): 190 if not shutdown_success: 191 192 # Always give compose down time to cleanup 193 timeout = max(timer.remaining(), COMPOSE_DOWN_RETRY_TIMEOUT) 194 try: 195 # 3. Cleanup after the test 196 subprocess.run(compose_down, check=True, timeout=timeout) 197 shutdown_success = True 198 199 except FileNotFoundError as err: 200 LOGGER.error("Bad docker-compose down command.\n") 201 LOGGER.exception(err) 202 203 except subprocess.CalledProcessError as err: 204 LOGGER.error("Failed to cleanup after test.") 205 LOGGER.exception(err) 206 207 except subprocess.TimeoutExpired as err: 208 LOGGER.error("Shutting down the test timed out.") 209 LOGGER.exception(err) 210 211 if not shutdown_success: 212 LOGGER.critical( 213 "There are residual containers on the host that need to be" 214 " cleaned up! Do `docker ps -a` and `docker newtork list` to" 215 " see what was left behind or use `run_docker_test --clean`!" 216 ) 217 218 if exit_status != 0: 219 LOGGER.error('Test %s failed', test_service_uppercase) 220 221 exit(exit_status) 222 223 except KeyboardInterrupt: 224 subprocess.run( 225 compose_down, 226 check=True, 227 timeout=COMPOSE_DOWN_RETRY_TIMEOUT) 228 exit(1) 229 230 231 def load_compose_file(compose_file): 232 233 try: 234 with open(compose_file) as fd: 235 contents = fd.read() 236 compose = yaml.load(contents) 237 return compose 238 239 except OSError: 240 raise RunDockerTestError( 241 "Docker compose file '{}' could not be opened. Make sure it " 242 "exists and is readable.".format(compose_file)) 243 244 245 def parse_args(): 246 parser = argparse.ArgumentParser() 247 248 parser.add_argument( 249 "compose_file", 250 help="docker-compose.yaml file that contains the test") 251 252 parser.add_argument( 253 "-c", "--clean", 254 help="don't run the test, just cleanup a previous run", 255 action='store_true', 256 default=False) 257 258 parser.add_argument( 259 "-n", "--no-build", 260 help="don't build docker images", 261 action='store_true', 262 default=False) 263 264 parser.add_argument( 265 "-t", "--timeout", 266 help="how long to wait before timing out", 267 type=int, 268 default=DEFAULT_TIMEOUT) 269 270 return parser.parse_args() 271 272 273 def _build_test_service_image(test_service, compose): 274 cmd = compose + ['build', test_service] 275 try: 276 build = subprocess.Popen( 277 cmd, stdout=subprocess.PIPE) 278 279 for line in build.stdout: 280 print("build_test_image | " + line.decode().strip()) 281 282 except subprocess.CalledProcessError as err: 283 LOGGER.error("Failed to build image for {}".format(test_service)) 284 LOGGER.exception(err) 285 raise 286 287 288 def _get_test_service(compose_file): 289 return os.path.basename( 290 compose_file 291 ).replace('.yaml', '').replace('_', '-') 292 293 294 def _get_test_service_image(compose_dict, test_service, compose_file): 295 if "image" not in compose_dict['services'][test_service]: 296 raise RunDockerTestError( 297 "Test service '{}' does not have an image specified: '{}'".format( 298 test_service, compose_file)) 299 else: 300 return compose_dict['services'][test_service]['image'].split(":")[0] 301 302 303 def _validate_compose_dict(compose_dict, test_service, compose_file): 304 if test_service not in compose_dict['services']: 305 raise RunDockerTestError( 306 "Test service '{}' does not exist in compose file: '{}'".format( 307 test_service, compose_file)) 308 309 310 def _check_for_existing_containers(compose_file, compose_dict, isolation_id): 311 containers = _get_existing_containers() 312 for service in compose_dict['services'].keys(): 313 container_name_to_create = "{}_{}_1".format(isolation_id, service) 314 for existing_container_name in containers: 315 if container_name_to_create == existing_container_name: 316 raise RunDockerTestError( 317 "The container '{}' which would be created by this test" 318 " already exists!\nDo:\n`run_docker_test --clean {}`\nto" 319 " remove the container, or use docker manually.".format( 320 container_name_to_create, compose_file 321 ) 322 ) 323 324 325 def _check_for_existing_image(test_service, isolation_id): 326 images = _get_existing_images() 327 image_to_create = '{}:{}'.format(test_service, isolation_id) 328 if image_to_create in images: 329 return True 330 331 332 def _check_for_existing_network(isolation_id, compose_file): 333 networks = _get_existing_networks() 334 network_to_create = '{}_default'.format(isolation_id) 335 if network_to_create in networks: 336 raise RunDockerTestError( 337 "The network '{}' which would be created by this test already" 338 " exists!\nDo:\n`run_docker_test --clean {}`\nto remove the" 339 " network, or use docker manually.".format( 340 network_to_create, compose_file 341 ) 342 ) 343 344 345 def _get_existing_containers(): 346 cmd = ['docker', 'ps', '-a', '--format={{.Names}}'] 347 success = False 348 try: 349 containers = subprocess.run( 350 cmd, stdout=subprocess.PIPE, check=True, timeout=DOCKER_PS_TIMEOUT 351 ).stdout.decode().strip().split('\n') 352 success = True 353 354 except FileNotFoundError as err: 355 LOGGER.error("Bad docker ps command") 356 LOGGER.exception(err) 357 358 except subprocess.CalledProcessError as err: 359 LOGGER.error("Failed to retrieve exit status of test.") 360 LOGGER.exception(err) 361 362 except subprocess.TimeoutExpired as err: 363 LOGGER.error("Retrieving exit status timed out.") 364 LOGGER.exception(err) 365 366 if not success: 367 raise RunDockerTestError("Failed to get list of docker containers.") 368 369 return containers 370 371 372 def _get_existing_images(): 373 cmd = ['docker', 'images', '--format={{.Repository}}:{{.Tag}}'] 374 success = False 375 try: 376 images = subprocess.run( 377 cmd, stdout=subprocess.PIPE, check=True, timeout=DOCKER_PS_TIMEOUT 378 ).stdout.decode().strip() 379 success = True 380 381 except FileNotFoundError as err: 382 LOGGER.error("Bad docker images command") 383 LOGGER.exception(err) 384 385 except subprocess.CalledProcessError as err: 386 LOGGER.error("Failed to retrieve image list.") 387 LOGGER.exception(err) 388 389 except subprocess.TimeoutExpired as err: 390 LOGGER.error("Retrieving image list timed out.") 391 LOGGER.exception(err) 392 393 if not success: 394 raise RunDockerTestError("Failed to get list of docker images.") 395 396 return images 397 398 399 def _get_existing_networks(): 400 cmd = ['docker', 'network', 'ls', '--format={{.Name}}'] 401 success = False 402 403 try: 404 networks = subprocess.run( 405 cmd, stdout=subprocess.PIPE, check=True, timeout=DOCKER_PS_TIMEOUT 406 ).stdout.decode().strip().split('\n') 407 success = True 408 409 except FileNotFoundError as err: 410 LOGGER.error("Bad docker network ls command") 411 LOGGER.exception(err) 412 413 except subprocess.CalledProcessError as err: 414 LOGGER.error("Failed to retrieve network list.") 415 LOGGER.exception(err) 416 417 except subprocess.TimeoutExpired as err: 418 LOGGER.error("Retrieving network list timed out.") 419 LOGGER.exception(err) 420 421 if not success: 422 raise RunDockerTestError("Failed to get list of docker networks.") 423 424 return networks 425 426 427 def _get_isolation_id(): 428 if 'ISOLATION_ID' in os.environ: 429 isolation_id = os.environ['ISOLATION_ID'] 430 else: 431 isolation_id = 'latest' 432 433 if not isolation_id.isalnum(): 434 raise RunDockerTestError("ISOLATION_ID must be alphanumeric") 435 436 return isolation_id 437 438 439 def _setup_environ(isolation_id): 440 os.environ['ISOLATION_ID'] = isolation_id 441 os.environ['SAWTOOTH_CORE'] = os.path.dirname( 442 os.path.dirname( 443 os.path.realpath(__file__) 444 ) 445 ) 446 print(os.environ['SAWTOOTH_CORE']) 447 448 449 def _get_compose_dir(): 450 return os.path.join( 451 os.path.dirname(os.path.dirname(os.path.realpath(__file__))), 452 "integration", "sawtooth_integration", "docker" 453 ) 454 455 456 def _get_compose_file(compose_file): 457 # Try "as given" 458 if not os.path.exists(compose_file): 459 # If not, try in integration docker directory 460 compose_file = os.path.join(_get_compose_dir(), compose_file) 461 if not os.path.exists(compose_file): 462 # If not, try appending .yaml 463 compose_file = os.path.join( 464 _get_compose_dir(), 465 "{}.yaml".format(compose_file) 466 ) 467 if not os.path.exists(compose_file): 468 raise RunDockerTestError( 469 "Docker compose file '{}' does not exist.".format(compose_file)) 470 471 return compose_file 472 473 474 if __name__ == "__main__": 475 try: 476 main() 477 478 except RunDockerTestError as err: 479 LOGGER.error(err) 480 exit(1)