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)