Home | History | Annotate | Download | only in server
      1 #!/usr/bin/python -u
      2 # Copyright 2007-2008 Martin J. Bligh <mbligh (at] google.com>, Google Inc.
      3 # Released under the GPL v2
      4 
      5 """
      6 Run a control file through the server side engine
      7 """
      8 
      9 import datetime
     10 import contextlib
     11 import getpass
     12 import logging
     13 import os
     14 import re
     15 import signal
     16 import socket
     17 import sys
     18 import traceback
     19 import time
     20 import urllib2
     21 
     22 
     23 import common
     24 from autotest_lib.client.common_lib import control_data
     25 from autotest_lib.client.common_lib import error
     26 from autotest_lib.client.common_lib import global_config
     27 from autotest_lib.client.common_lib import utils
     28 from autotest_lib.client.common_lib.cros.graphite import autotest_es
     29 
     30 try:
     31     from chromite.lib import metrics
     32 except ImportError:
     33     metrics = utils.metrics_mock
     34 
     35 try:
     36     from autotest_lib.puppylab import results_mocker
     37 except ImportError:
     38     results_mocker = None
     39 
     40 _CONFIG = global_config.global_config
     41 
     42 
     43 # Number of seconds to wait before returning if testing mode is enabled
     44 TESTING_MODE_SLEEP_SECS = 1
     45 
     46 
     47 from autotest_lib.server import frontend
     48 from autotest_lib.server import server_logging_config
     49 from autotest_lib.server import server_job, utils, autoserv_parser, autotest
     50 from autotest_lib.server import utils as server_utils
     51 from autotest_lib.server import site_utils
     52 from autotest_lib.server.cros.dynamic_suite import frontend_wrappers
     53 from autotest_lib.site_utils import job_directories
     54 from autotest_lib.site_utils import job_overhead
     55 from autotest_lib.site_utils import lxc
     56 from autotest_lib.site_utils import lxc_utils
     57 from autotest_lib.client.common_lib import pidfile, logging_manager
     58 
     59 
     60 # Control segment to stage server-side package.
     61 STAGE_SERVER_SIDE_PACKAGE_CONTROL_FILE = server_job._control_segment_path(
     62         'stage_server_side_package')
     63 
     64 # Command line to start servod in a moblab.
     65 START_SERVOD_CMD = 'sudo start servod BOARD=%s PORT=%s'
     66 STOP_SERVOD_CMD = 'sudo stop servod'
     67 
     68 def log_alarm(signum, frame):
     69     logging.error("Received SIGALARM. Ignoring and continuing on.")
     70     sys.exit(1)
     71 
     72 
     73 def _get_machines(parser):
     74     """Get a list of machine names from command line arg -m or a file.
     75 
     76     @param parser: Parser for the command line arguments.
     77 
     78     @return: A list of machine names from command line arg -m or the
     79              machines file specified in the command line arg -M.
     80     """
     81     if parser.options.machines:
     82         machines = parser.options.machines.replace(',', ' ').strip().split()
     83     else:
     84         machines = []
     85     machines_file = parser.options.machines_file
     86     if machines_file:
     87         machines = []
     88         for m in open(machines_file, 'r').readlines():
     89             # remove comments, spaces
     90             m = re.sub('#.*', '', m).strip()
     91             if m:
     92                 machines.append(m)
     93         logging.debug('Read list of machines from file: %s', machines_file)
     94         logging.debug('Machines: %s', ','.join(machines))
     95 
     96     if machines:
     97         for machine in machines:
     98             if not machine or re.search('\s', machine):
     99                 parser.parser.error("Invalid machine: %s" % str(machine))
    100         machines = list(set(machines))
    101         machines.sort()
    102     return machines
    103 
    104 
    105 def _stage_ssp(parser):
    106     """Stage server-side package.
    107 
    108     This function calls a control segment to stage server-side package based on
    109     the job and autoserv command line option. The detail implementation could
    110     be different for each host type. Currently, only CrosHost has
    111     stage_server_side_package function defined.
    112     The script returns None if no server-side package is available. However,
    113     it may raise exception if it failed for reasons other than artifact (the
    114     server-side package) not found.
    115 
    116     @param parser: Command line arguments parser passed in the autoserv process.
    117 
    118     @return: (ssp_url, error_msg), where
    119               ssp_url is a url to the autotest server-side package. None if
    120               server-side package is not supported.
    121               error_msg is a string indicating the failures. None if server-
    122               side package is staged successfully.
    123     """
    124     machines_list = _get_machines(parser)
    125     machines_list = server_job.get_machine_dicts(
    126             machines_list, parser.options.lab, parser.options.host_attributes)
    127 
    128     # If test_source_build is not specified, default to use server-side test
    129     # code from build specified in --image.
    130     namespace = {'machines': machines_list,
    131                  'image': (parser.options.test_source_build or
    132                            parser.options.image),}
    133     script_locals = {}
    134     execfile(STAGE_SERVER_SIDE_PACKAGE_CONTROL_FILE, namespace, script_locals)
    135     return script_locals['ssp_url'], script_locals['error_msg']
    136 
    137 
    138 def _run_with_ssp(job, container_name, job_id, results, parser, ssp_url,
    139                   job_folder, machines):
    140     """Run the server job with server-side packaging.
    141 
    142     @param job: The server job object.
    143     @param container_name: Name of the container to run the test.
    144     @param job_id: ID of the test job.
    145     @param results: Folder to store results. This could be different from
    146                     parser.options.results:
    147                     parser.options.results  can be set to None for results to be
    148                     stored in a temp folder.
    149                     results can be None for autoserv run requires no logging.
    150     @param parser: Command line parser that contains the options.
    151     @param ssp_url: url of the staged server-side package.
    152     @param job_folder: Name of the job result folder.
    153     @param machines: A list of machines to run the test.
    154     """
    155     bucket = lxc.ContainerBucket()
    156     control = (parser.args[0] if len(parser.args) > 0 and parser.args[0] != ''
    157                else None)
    158     try:
    159         dut_name = machines[0] if len(machines) >= 1 else None
    160         test_container = bucket.setup_test(container_name, job_id, ssp_url,
    161                                            results, control=control,
    162                                            job_folder=job_folder,
    163                                            dut_name=dut_name)
    164     except Exception as e:
    165         job.record('FAIL', None, None,
    166                    'Failed to setup container for test: %s. Check logs in '
    167                    'ssp_logs folder for more details.' % e)
    168         raise
    169 
    170     args = sys.argv[:]
    171     args.remove('--require-ssp')
    172     # --parent_job_id is only useful in autoserv running in host, not in
    173     # container. Include this argument will cause test to fail for builds before
    174     # CL 286265 was merged.
    175     if '--parent_job_id' in args:
    176         index = args.index('--parent_job_id')
    177         args.remove('--parent_job_id')
    178         # Remove the actual parent job id in command line arg.
    179         del args[index]
    180 
    181     # A dictionary of paths to replace in the command line. Key is the path to
    182     # be replaced with the one in value.
    183     paths_to_replace = {}
    184     # Replace the control file path with the one in container.
    185     if control:
    186         container_control_filename = os.path.join(
    187                 lxc.CONTROL_TEMP_PATH, os.path.basename(control))
    188         paths_to_replace[control] = container_control_filename
    189     # Update result directory with the one in container.
    190     container_result_dir = os.path.join(lxc.RESULT_DIR_FMT % job_folder)
    191     if parser.options.results:
    192         paths_to_replace[parser.options.results] = container_result_dir
    193     # Update parse_job directory with the one in container. The assumption is
    194     # that the result folder to be parsed is always the same as the results_dir.
    195     if parser.options.parse_job:
    196         paths_to_replace[parser.options.parse_job] = container_result_dir
    197 
    198     args = [paths_to_replace.get(arg, arg) for arg in args]
    199 
    200     # Apply --use-existing-results, results directory is aready created and
    201     # mounted in container. Apply this arg to avoid exception being raised.
    202     if not '--use-existing-results' in args:
    203         args.append('--use-existing-results')
    204 
    205     # Make sure autoserv running in container using a different pid file.
    206     if not '--pidfile-label' in args:
    207         args.extend(['--pidfile-label', 'container_autoserv'])
    208 
    209     cmd_line = ' '.join(["'%s'" % arg if ' ' in arg else arg for arg in args])
    210     logging.info('Run command in container: %s', cmd_line)
    211     success = False
    212     try:
    213         test_container.attach_run(cmd_line)
    214         success = True
    215     except Exception as e:
    216         # If the test run inside container fails without generating any log,
    217         # write a message to status.log to help troubleshooting.
    218         debug_files = os.listdir(os.path.join(results, 'debug'))
    219         if not debug_files:
    220             job.record('FAIL', None, None,
    221                        'Failed to run test inside the container: %s. Check '
    222                        'logs in ssp_logs folder for more details.' % e)
    223         raise
    224     finally:
    225         metrics.Counter(
    226             'chromeos/autotest/experimental/execute_job_in_ssp').increment(
    227                 fields={'success': success})
    228         # metadata is uploaded separately so it can use http to upload.
    229         metadata = {'drone': socket.gethostname(),
    230                     'job_id': job_id,
    231                     'success': success}
    232         autotest_es.post(use_http=True,
    233                          type_str=lxc.CONTAINER_RUN_TEST_METADB_TYPE,
    234                          metadata=metadata)
    235         test_container.destroy()
    236 
    237 
    238 def correct_results_folder_permission(results):
    239     """Make sure the results folder has the right permission settings.
    240 
    241     For tests running with server-side packaging, the results folder has the
    242     owner of root. This must be changed to the user running the autoserv
    243     process, so parsing job can access the results folder.
    244     TODO(dshi): crbug.com/459344 Remove this function when test container can be
    245     unprivileged container.
    246 
    247     @param results: Path to the results folder.
    248 
    249     """
    250     if not results:
    251         return
    252 
    253     try:
    254         utils.run('sudo -n chown -R %s "%s"' % (os.getuid(), results))
    255         utils.run('sudo -n chgrp -R %s "%s"' % (os.getgid(), results))
    256     except error.CmdError as e:
    257         metadata = {'error': str(e),
    258                     'result_folder': results,
    259                     'drone': socket.gethostname()}
    260         autotest_es.post(use_http=True, type_str='correct_results_folder_failure',
    261                          metadata=metadata)
    262         raise
    263 
    264 
    265 def _start_servod(machine):
    266     """Try to start servod in moblab if it's not already running or running with
    267     different board or port.
    268 
    269     @param machine: Name of the dut used for test.
    270     """
    271     if not utils.is_moblab():
    272         return
    273 
    274     logging.debug('Trying to start servod.')
    275     try:
    276         afe = frontend.AFE()
    277         board = server_utils.get_board_from_afe(machine, afe)
    278         hosts = afe.get_hosts(hostname=machine)
    279         servo_host = hosts[0].attributes.get('servo_host', None)
    280         servo_port = hosts[0].attributes.get('servo_port', 9999)
    281         if not servo_host in ['localhost', '127.0.0.1']:
    282             logging.warn('Starting servod is aborted. The dut\'s servo_host '
    283                          'attribute is not set to localhost.')
    284             return
    285     except (urllib2.HTTPError, urllib2.URLError):
    286         # Ignore error if RPC failed to get board
    287         logging.error('Failed to get board name from AFE. Start servod is '
    288                       'aborted')
    289         return
    290 
    291     try:
    292         pid = utils.run('pgrep servod').stdout
    293         cmd_line = utils.run('ps -fp %s' % pid).stdout
    294         if ('--board %s' % board in cmd_line and
    295             '--port %s' % servo_port in cmd_line):
    296             logging.debug('Servod is already running with given board and port.'
    297                           ' There is no need to restart servod.')
    298             return
    299         logging.debug('Servod is running with different board or port. '
    300                       'Stopping existing servod.')
    301         utils.run('sudo stop servod')
    302     except error.CmdError:
    303         # servod is not running.
    304         pass
    305 
    306     try:
    307         utils.run(START_SERVOD_CMD % (board, servo_port))
    308         logging.debug('Servod is started')
    309     except error.CmdError as e:
    310         logging.error('Servod failed to be started, error: %s', e)
    311 
    312 
    313 def run_autoserv(pid_file_manager, results, parser, ssp_url, use_ssp):
    314     """Run server job with given options.
    315 
    316     @param pid_file_manager: PidFileManager used to monitor the autoserv process
    317     @param results: Folder to store results.
    318     @param parser: Parser for the command line arguments.
    319     @param ssp_url: Url to server-side package.
    320     @param use_ssp: Set to True to run with server-side packaging.
    321     """
    322     if parser.options.warn_no_ssp:
    323         # Post a warning in the log.
    324         logging.warn('Autoserv is required to run with server-side packaging. '
    325                      'However, no drone is found to support server-side '
    326                      'packaging. The test will be executed in a drone without '
    327                      'server-side packaging supported.')
    328 
    329     # send stdin to /dev/null
    330     dev_null = os.open(os.devnull, os.O_RDONLY)
    331     os.dup2(dev_null, sys.stdin.fileno())
    332     os.close(dev_null)
    333 
    334     # Create separate process group if the process is not a process group
    335     # leader. This allows autoserv process to keep running after the caller
    336     # process (drone manager call) exits.
    337     if os.getpid() != os.getpgid(0):
    338         os.setsid()
    339 
    340     # Container name is predefined so the container can be destroyed in
    341     # handle_sigterm.
    342     job_or_task_id = job_directories.get_job_id_or_task_id(
    343             parser.options.results)
    344     container_name = (lxc.TEST_CONTAINER_NAME_FMT %
    345                       (job_or_task_id, time.time(), os.getpid()))
    346     job_folder = job_directories.get_job_folder_name(parser.options.results)
    347 
    348     # Implement SIGTERM handler
    349     def handle_sigterm(signum, frame):
    350         logging.debug('Received SIGTERM')
    351         if pid_file_manager:
    352             pid_file_manager.close_file(1, signal.SIGTERM)
    353         logging.debug('Finished writing to pid_file. Killing process.')
    354 
    355         # Update results folder's file permission. This needs to be done ASAP
    356         # before the parsing process tries to access the log.
    357         if use_ssp and results:
    358             correct_results_folder_permission(results)
    359 
    360         # TODO (sbasi) - remove the time.sleep when crbug.com/302815 is solved.
    361         # This sleep allows the pending output to be logged before the kill
    362         # signal is sent.
    363         time.sleep(.1)
    364         if use_ssp:
    365             logging.debug('Destroy container %s before aborting the autoserv '
    366                           'process.', container_name)
    367             metadata = {'drone': socket.gethostname(),
    368                         'job_id': job_or_task_id,
    369                         'container_name': container_name,
    370                         'action': 'abort',
    371                         'success': True}
    372             try:
    373                 bucket = lxc.ContainerBucket()
    374                 container = bucket.get(container_name)
    375                 if container:
    376                     container.destroy()
    377                 else:
    378                     metadata['success'] = False
    379                     metadata['error'] = 'container not found'
    380                     logging.debug('Container %s is not found.', container_name)
    381             except:
    382                 metadata['success'] = False
    383                 metadata['error'] = 'Exception: %s' % str(sys.exc_info())
    384                 # Handle any exception so the autoserv process can be aborted.
    385                 logging.exception('Failed to destroy container %s.',
    386                                   container_name)
    387             autotest_es.post(use_http=True,
    388                              type_str=lxc.CONTAINER_RUN_TEST_METADB_TYPE,
    389                              metadata=metadata)
    390             # Try to correct the result file permission again after the
    391             # container is destroyed, as the container might have created some
    392             # new files in the result folder.
    393             if results:
    394                 correct_results_folder_permission(results)
    395 
    396         os.killpg(os.getpgrp(), signal.SIGKILL)
    397 
    398     # Set signal handler
    399     signal.signal(signal.SIGTERM, handle_sigterm)
    400 
    401     # faulthandler is only needed to debug in the Lab and is not avaliable to
    402     # be imported in the chroot as part of VMTest, so Try-Except it.
    403     try:
    404         import faulthandler
    405         faulthandler.register(signal.SIGTERM, all_threads=True, chain=True)
    406         logging.debug('faulthandler registered on SIGTERM.')
    407     except ImportError:
    408         sys.exc_clear()
    409 
    410     # Ignore SIGTTOU's generated by output from forked children.
    411     signal.signal(signal.SIGTTOU, signal.SIG_IGN)
    412 
    413     # If we received a SIGALARM, let's be loud about it.
    414     signal.signal(signal.SIGALRM, log_alarm)
    415 
    416     # Server side tests that call shell scripts often depend on $USER being set
    417     # but depending on how you launch your autotest scheduler it may not be set.
    418     os.environ['USER'] = getpass.getuser()
    419 
    420     label = parser.options.label
    421     group_name = parser.options.group_name
    422     user = parser.options.user
    423     client = parser.options.client
    424     server = parser.options.server
    425     install_before = parser.options.install_before
    426     install_after = parser.options.install_after
    427     verify = parser.options.verify
    428     repair = parser.options.repair
    429     cleanup = parser.options.cleanup
    430     provision = parser.options.provision
    431     reset = parser.options.reset
    432     job_labels = parser.options.job_labels
    433     no_tee = parser.options.no_tee
    434     parse_job = parser.options.parse_job
    435     execution_tag = parser.options.execution_tag
    436     if not execution_tag:
    437         execution_tag = parse_job
    438     ssh_user = parser.options.ssh_user
    439     ssh_port = parser.options.ssh_port
    440     ssh_pass = parser.options.ssh_pass
    441     collect_crashinfo = parser.options.collect_crashinfo
    442     control_filename = parser.options.control_filename
    443     test_retry = parser.options.test_retry
    444     verify_job_repo_url = parser.options.verify_job_repo_url
    445     skip_crash_collection = parser.options.skip_crash_collection
    446     ssh_verbosity = int(parser.options.ssh_verbosity)
    447     ssh_options = parser.options.ssh_options
    448     no_use_packaging = parser.options.no_use_packaging
    449     host_attributes = parser.options.host_attributes
    450     in_lab = bool(parser.options.lab)
    451 
    452     # can't be both a client and a server side test
    453     if client and server:
    454         parser.parser.error("Can not specify a test as both server and client!")
    455 
    456     if provision and client:
    457         parser.parser.error("Cannot specify provisioning and client!")
    458 
    459     is_special_task = (verify or repair or cleanup or collect_crashinfo or
    460                        provision or reset)
    461     if len(parser.args) < 1 and not is_special_task:
    462         parser.parser.error("Missing argument: control file")
    463 
    464     if ssh_verbosity > 0:
    465         # ssh_verbosity is an integer between 0 and 3, inclusive
    466         ssh_verbosity_flag = '-' + 'v' * ssh_verbosity
    467     else:
    468         ssh_verbosity_flag = ''
    469 
    470     # We have a control file unless it's just a verify/repair/cleanup job
    471     if len(parser.args) > 0:
    472         control = parser.args[0]
    473     else:
    474         control = None
    475 
    476     machines = _get_machines(parser)
    477     if group_name and len(machines) < 2:
    478         parser.parser.error('-G %r may only be supplied with more than one '
    479                             'machine.' % group_name)
    480 
    481     kwargs = {'group_name': group_name, 'tag': execution_tag,
    482               'disable_sysinfo': parser.options.disable_sysinfo}
    483     if parser.options.parent_job_id:
    484         kwargs['parent_job_id'] = int(parser.options.parent_job_id)
    485     if control_filename:
    486         kwargs['control_filename'] = control_filename
    487     if host_attributes:
    488         kwargs['host_attributes'] = host_attributes
    489     kwargs['in_lab'] = in_lab
    490     job = server_job.server_job(control, parser.args[1:], results, label,
    491                                 user, machines, client, parse_job,
    492                                 ssh_user, ssh_port, ssh_pass,
    493                                 ssh_verbosity_flag, ssh_options,
    494                                 test_retry, **kwargs)
    495 
    496     job.logging.start_logging()
    497     job.init_parser()
    498 
    499     # perform checks
    500     job.precheck()
    501 
    502     # run the job
    503     exit_code = 0
    504     auto_start_servod = _CONFIG.get_config_value(
    505             'AUTOSERV', 'auto_start_servod', type=bool, default=False)
    506 
    507     site_utils.SetupTsMonGlobalState('autoserv', indirect=False,
    508                                      short_lived=True)
    509     try:
    510         try:
    511             if repair:
    512                 if auto_start_servod and len(machines) == 1:
    513                     _start_servod(machines[0])
    514                 job.repair(job_labels)
    515             elif verify:
    516                 job.verify(job_labels)
    517             elif provision:
    518                 job.provision(job_labels)
    519             elif reset:
    520                 job.reset(job_labels)
    521             elif cleanup:
    522                 job.cleanup(job_labels)
    523             else:
    524                 if auto_start_servod and len(machines) == 1:
    525                     _start_servod(machines[0])
    526                 if use_ssp:
    527                     try:
    528                         _run_with_ssp(job, container_name, job_or_task_id,
    529                                         results, parser, ssp_url, job_folder,
    530                                         machines)
    531                     finally:
    532                         # Update the ownership of files in result folder.
    533                         correct_results_folder_permission(results)
    534                 else:
    535                     if collect_crashinfo:
    536                         # Update the ownership of files in result folder. If the
    537                         # job to collect crashinfo was running inside container
    538                         # (SSP) and crashed before correcting folder permission,
    539                         # the result folder might have wrong permission setting.
    540                         try:
    541                             correct_results_folder_permission(results)
    542                         except:
    543                             # Ignore any error as the user may not have root
    544                             # permission to run sudo command.
    545                             pass
    546                     metric_name = ('chromeos/autotest/experimental/'
    547                                    'autoserv_job_run_duration')
    548                     f = {'in_container': utils.is_in_container(),
    549                          'success': False}
    550                     with metrics.SecondsTimer(metric_name, fields=f) as c:
    551                         job.run(install_before, install_after,
    552                                 verify_job_repo_url=verify_job_repo_url,
    553                                 only_collect_crashinfo=collect_crashinfo,
    554                                 skip_crash_collection=skip_crash_collection,
    555                                 job_labels=job_labels,
    556                                 use_packaging=(not no_use_packaging))
    557                         c['success'] = True
    558 
    559         finally:
    560             while job.hosts:
    561                 host = job.hosts.pop()
    562                 host.close()
    563     except:
    564         exit_code = 1
    565         traceback.print_exc()
    566     finally:
    567         metrics.Flush()
    568 
    569     if pid_file_manager:
    570         pid_file_manager.num_tests_failed = job.num_tests_failed
    571         pid_file_manager.close_file(exit_code)
    572     job.cleanup_parser()
    573 
    574     sys.exit(exit_code)
    575 
    576 
    577 def record_autoserv(options, duration_secs):
    578     """Record autoserv end-to-end time in metadata db.
    579 
    580     @param options: parser options.
    581     @param duration_secs: How long autoserv has taken, in secs.
    582     """
    583     # Get machine hostname
    584     machines = options.machines.replace(
    585             ',', ' ').strip().split() if options.machines else []
    586     num_machines = len(machines)
    587     if num_machines > 1:
    588         # Skip the case where atomic group is used.
    589         return
    590     elif num_machines == 0:
    591         machines.append('hostless')
    592 
    593     # Determine the status that will be reported.
    594     s = job_overhead.STATUS
    595     task_mapping = {
    596             'reset': s.RESETTING, 'verify': s.VERIFYING,
    597             'provision': s.PROVISIONING, 'repair': s.REPAIRING,
    598             'cleanup': s.CLEANING, 'collect_crashinfo': s.GATHERING}
    599     match = filter(lambda task: getattr(options, task, False) == True,
    600                    task_mapping)
    601     status = task_mapping[match[0]] if match else s.RUNNING
    602     is_special_task = status not in [s.RUNNING, s.GATHERING]
    603     job_or_task_id = job_directories.get_job_id_or_task_id(options.results)
    604     job_overhead.record_state_duration(
    605             job_or_task_id, machines[0], status, duration_secs,
    606             is_special_task=is_special_task)
    607 
    608 
    609 def main():
    610     start_time = datetime.datetime.now()
    611     # grab the parser
    612     parser = autoserv_parser.autoserv_parser
    613     parser.parse_args()
    614 
    615     if len(sys.argv) == 1:
    616         parser.parser.print_help()
    617         sys.exit(1)
    618 
    619     # If the job requires to run with server-side package, try to stage server-
    620     # side package first. If that fails with error that autotest server package
    621     # does not exist, fall back to run the job without using server-side
    622     # packaging. If option warn_no_ssp is specified, that means autoserv is
    623     # running in a drone does not support SSP, thus no need to stage server-side
    624     # package.
    625     ssp_url = None
    626     ssp_url_warning = False
    627     if (not parser.options.warn_no_ssp and parser.options.require_ssp):
    628         ssp_url, ssp_error_msg = _stage_ssp(parser)
    629         # The build does not have autotest server package. Fall back to not
    630         # to use server-side package. Logging is postponed until logging being
    631         # set up.
    632         ssp_url_warning = not ssp_url
    633 
    634     if parser.options.no_logging:
    635         results = None
    636     else:
    637         results = parser.options.results
    638         if not results:
    639             results = 'results.' + time.strftime('%Y-%m-%d-%H.%M.%S')
    640         results = os.path.abspath(results)
    641         resultdir_exists = False
    642         for filename in ('control.srv', 'status.log', '.autoserv_execute'):
    643             if os.path.exists(os.path.join(results, filename)):
    644                 resultdir_exists = True
    645         if not parser.options.use_existing_results and resultdir_exists:
    646             error = "Error: results directory already exists: %s\n" % results
    647             sys.stderr.write(error)
    648             sys.exit(1)
    649 
    650         # Now that we certified that there's no leftover results dir from
    651         # previous jobs, lets create the result dir since the logging system
    652         # needs to create the log file in there.
    653         if not os.path.isdir(results):
    654             os.makedirs(results)
    655 
    656     # Server-side packaging will only be used if it's required and the package
    657     # is available. If warn_no_ssp is specified, it means that autoserv is
    658     # running in a drone does not have SSP supported and a warning will be logs.
    659     # Therefore, it should not run with SSP.
    660     use_ssp = (not parser.options.warn_no_ssp and parser.options.require_ssp
    661                and ssp_url)
    662     if use_ssp:
    663         log_dir = os.path.join(results, 'ssp_logs') if results else None
    664         if log_dir and not os.path.exists(log_dir):
    665             os.makedirs(log_dir)
    666     else:
    667         log_dir = results
    668 
    669     logging_manager.configure_logging(
    670             server_logging_config.ServerLoggingConfig(),
    671             results_dir=log_dir,
    672             use_console=not parser.options.no_tee,
    673             verbose=parser.options.verbose,
    674             no_console_prefix=parser.options.no_console_prefix)
    675 
    676     if ssp_url_warning:
    677         logging.warn(
    678                 'Autoserv is required to run with server-side packaging. '
    679                 'However, no server-side package can be found based on '
    680                 '`--image`, host attribute job_repo_url or host OS version '
    681                 'label. It could be that the build to test is older than the '
    682                 'minimum version that supports server-side packaging. The test '
    683                 'will be executed without using erver-side packaging. '
    684                 'Following is the detailed error:\n%s', ssp_error_msg)
    685 
    686     if results:
    687         logging.info("Results placed in %s" % results)
    688 
    689         # wait until now to perform this check, so it get properly logged
    690         if (parser.options.use_existing_results and not resultdir_exists and
    691             not utils.is_in_container()):
    692             logging.error("No existing results directory found: %s", results)
    693             sys.exit(1)
    694 
    695     logging.debug('autoserv is running in drone %s.', socket.gethostname())
    696     logging.debug('autoserv command was: %s', ' '.join(sys.argv))
    697 
    698     if parser.options.write_pidfile and results:
    699         pid_file_manager = pidfile.PidFileManager(parser.options.pidfile_label,
    700                                                   results)
    701         pid_file_manager.open_file()
    702     else:
    703         pid_file_manager = None
    704 
    705     autotest.BaseAutotest.set_install_in_tmpdir(
    706         parser.options.install_in_tmpdir)
    707 
    708     try:
    709         # Take the first argument as control file name, get the test name from
    710         # the control file.
    711         if (len(parser.args) > 0 and parser.args[0] != '' and
    712             parser.options.machines):
    713             try:
    714                 test_name = control_data.parse_control(parser.args[0],
    715                                                        raise_warnings=True).name
    716             except control_data.ControlVariableException:
    717                 logging.debug('Failed to retrieve test name from control file.')
    718                 test_name = None
    719     except control_data.ControlVariableException as e:
    720         logging.error(str(e))
    721     exit_code = 0
    722     # TODO(beeps): Extend this to cover different failure modes.
    723     # Testing exceptions are matched against labels sent to autoserv. Eg,
    724     # to allow only the hostless job to run, specify
    725     # testing_exceptions: test_suite in the shadow_config. To allow both
    726     # the hostless job and dummy_Pass to run, specify
    727     # testing_exceptions: test_suite,dummy_Pass. You can figure out
    728     # what label autoserv is invoked with by looking through the logs of a test
    729     # for the autoserv command's -l option.
    730     testing_exceptions = _CONFIG.get_config_value(
    731             'AUTOSERV', 'testing_exceptions', type=list, default=[])
    732     test_mode = _CONFIG.get_config_value(
    733             'AUTOSERV', 'testing_mode', type=bool, default=False)
    734     test_mode = (results_mocker and test_mode and not
    735                  any([ex in parser.options.label
    736                       for ex in testing_exceptions]))
    737     is_task = (parser.options.verify or parser.options.repair or
    738                parser.options.provision or parser.options.reset or
    739                parser.options.cleanup or parser.options.collect_crashinfo)
    740     try:
    741         try:
    742             if test_mode:
    743                 # The parser doesn't run on tasks anyway, so we can just return
    744                 # happy signals without faking results.
    745                 if not is_task:
    746                     machine = parser.options.results.split('/')[-1]
    747 
    748                     # TODO(beeps): The proper way to do this would be to
    749                     # refactor job creation so we can invoke job.record
    750                     # directly. To do that one needs to pipe the test_name
    751                     # through run_autoserv and bail just before invoking
    752                     # the server job. See the comment in
    753                     # puppylab/results_mocker for more context.
    754                     results_mocker.ResultsMocker(
    755                             test_name if test_name else 'unknown-test',
    756                             parser.options.results, machine
    757                             ).mock_results()
    758                 return
    759             else:
    760                 run_autoserv(pid_file_manager, results, parser, ssp_url,
    761                              use_ssp)
    762         except SystemExit as e:
    763             exit_code = e.code
    764             if exit_code:
    765                 logging.exception(e)
    766         except Exception as e:
    767             # If we don't know what happened, we'll classify it as
    768             # an 'abort' and return 1.
    769             logging.exception(e)
    770             exit_code = 1
    771     finally:
    772         if pid_file_manager:
    773             pid_file_manager.close_file(exit_code)
    774         # Record the autoserv duration time. Must be called
    775         # just before the system exits to ensure accuracy.
    776         duration_secs = (datetime.datetime.now() - start_time).total_seconds()
    777         record_autoserv(parser.options, duration_secs)
    778     sys.exit(exit_code)
    779 
    780 
    781 if __name__ == '__main__':
    782     main()
    783