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