1#!/usr/bin/python2 -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 shutil
16import signal
17import socket
18import sys
19import traceback
20import time
21import six
22from six.moves import urllib
23
24import common
25from autotest_lib.client.bin.result_tools import utils as result_utils
26from autotest_lib.client.bin.result_tools import view as result_view
27from autotest_lib.client.common_lib import control_data
28from autotest_lib.client.common_lib import autotest_enum
29from autotest_lib.client.common_lib import error
30from autotest_lib.client.common_lib import global_config
31from autotest_lib.client.common_lib import host_queue_entry_states
32from autotest_lib.client.common_lib import host_states
33from autotest_lib.client.common_lib import seven
34from autotest_lib.server.cros.dynamic_suite import suite
35
36try:
37    from chromite.lib import metrics
38    from chromite.lib import cloud_trace
39except ImportError:
40    from autotest_lib.client.common_lib import utils as common_utils
41    metrics = common_utils.metrics_mock
42    import mock
43    cloud_trace = mock.MagicMock()
44
45_CONFIG = global_config.global_config
46
47# Number of seconds to wait before returning if testing mode is enabled
48TESTING_MODE_SLEEP_SECS = 1
49
50
51from autotest_lib.server import frontend
52from autotest_lib.server import server_logging_config
53from autotest_lib.server import server_job, utils, autoserv_parser, autotest
54from autotest_lib.server import utils as server_utils
55from autotest_lib.server import site_utils
56from autotest_lib.server.cros.dynamic_suite import frontend_wrappers
57from autotest_lib.site_utils import job_directories
58from autotest_lib.site_utils import lxc
59from autotest_lib.site_utils.lxc import utils as lxc_utils
60from autotest_lib.client.common_lib import pidfile, logging_manager
61
62
63# Control segment to stage server-side package.
64STAGE_SERVER_SIDE_PACKAGE_CONTROL_FILE = server_job._control_segment_path(
65        'stage_server_side_package')
66
67# Command line to start servod in a moblab.
68START_SERVOD_CMD = 'sudo start servod BOARD=%s PORT=%s'
69STOP_SERVOD_CMD = 'sudo stop servod'
70
71_AUTOTEST_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), '..'))
72_CONTROL_FILE_FROM_CONTROL_NAME = 'control.from_control_name'
73
74_LXC_JOB_FOLDER = 'lxc_job_folder'
75
76def log_alarm(signum, frame):
77    logging.error("Received SIGALARM. Ignoring and continuing on.")
78    sys.exit(1)
79
80
81def _get_machines(parser):
82    """Get a list of machine names from command line arg -m or a file.
83
84    @param parser: Parser for the command line arguments.
85
86    @return: A list of machine names from command line arg -m or the
87             machines file specified in the command line arg -M.
88    """
89    if parser.options.machines:
90        machines = parser.options.machines.replace(',', ' ').strip().split()
91    else:
92        machines = []
93    machines_file = parser.options.machines_file
94    if machines_file:
95        machines = []
96        for m in open(machines_file, 'r').readlines():
97            # remove comments, spaces
98            m = re.sub('#.*', '', m).strip()
99            if m:
100                machines.append(m)
101        logging.debug('Read list of machines from file: %s', machines_file)
102        logging.debug('Machines: %s', ','.join(machines))
103
104    if machines:
105        for machine in machines:
106            if not machine or re.search('\s', machine):
107                parser.parser.error("Invalid machine: %s" % str(machine))
108        machines = list(set(machines))
109        machines.sort()
110    return machines
111
112
113def _stage_ssp(parser, resultsdir):
114    """Stage server-side package.
115
116    This function calls a control segment to stage server-side package based on
117    the job and autoserv command line option. The detail implementation could
118    be different for each host type. Currently, only CrosHost has
119    stage_server_side_package function defined.
120    The script returns None if no server-side package is available. However,
121    it may raise exception if it failed for reasons other than artifact (the
122    server-side package) not found.
123
124    @param parser: Command line arguments parser passed in the autoserv process.
125    @param resultsdir: Folder to store results. This could be different from
126            parser.options.results: parser.options.results  can be set to None
127            for results to be stored in a temp folder. resultsdir can be None
128            for autoserv run requires no logging.
129
130    @return: url to the autotest server-side package. None in case of errors.
131    """
132    machines_list = _get_machines(parser)
133    machines_list = server_job.get_machine_dicts(
134            machine_names=machines_list,
135            store_dir=os.path.join(resultsdir, parser.options.host_info_subdir),
136            in_lab=parser.options.lab,
137            use_shadow_store=not parser.options.local_only_host_info,
138            host_attributes=parser.options.host_attributes,
139    )
140
141    namespace = {'machines': machines_list,
142                 'isolate_hash': parser.options.isolate,
143                 'image': parser.options.test_source_build}
144    script_locals = {}
145
146    seven.exec_file(
147        STAGE_SERVER_SIDE_PACKAGE_CONTROL_FILE,
148        globals_=namespace,
149        locals_=script_locals,
150    )
151    ssp_url = script_locals['ssp_url']
152    if not ssp_url:
153        logging.error('Failed to stage SSP package: %s',
154                      script_locals['error_msg'])
155        logging.error('This job will fail later, when attempting to run with'
156                      ' SSP')
157    return ssp_url
158
159
160def _run_with_ssp(job, container_id, job_id, results, parser, ssp_url,
161                  machines):
162    """Run the server job with server-side packaging.
163
164    @param job: The server job object.
165    @param container_id: ID of the container to run the test.
166    @param job_id: ID of the test job.
167    @param results: Folder to store results. This could be different from
168                    parser.options.results:
169                    parser.options.results  can be set to None for results to be
170                    stored in a temp folder.
171                    results can be None if the autoserv run requires no logging.
172    @param parser: Command line parser that contains the options.
173    @param ssp_url: url of the staged server-side package.
174    @param machines: A list of machines to run the test.
175    """
176    if not ssp_url:
177        job.record('FAIL', None, None,
178                   'Failed to stage server-side package')
179        raise error.AutoservError('Failed to stage server-side package')
180
181    bucket = lxc.ContainerBucket(
182            base_name=_ssp_base_image_name_or_default(parser.options))
183    control = (parser.args[0] if len(parser.args) > 0 and parser.args[0] != ''
184               else None)
185    try:
186        dut_name = machines[0] if len(machines) >= 1 else None
187        test_container = bucket.setup_test(container_id, job_id, ssp_url,
188                                           results, control=control,
189                                           job_folder=_LXC_JOB_FOLDER,
190                                           dut_name=dut_name,
191                                           isolate_hash=parser.options.isolate)
192    except Exception as e:
193        job.record('FAIL', None, None,
194                   'Failed to setup container for test: %s. Check logs in '
195                   'ssp_logs folder for more details.' % e)
196        raise
197
198    args = sys.argv[:]
199    args.remove('--require-ssp')
200    # --parent_job_id is only useful in autoserv running in host, not in
201    # container. Include this argument will cause test to fail for builds before
202    # CL 286265 was merged.
203    if '--parent_job_id' in args:
204        index = args.index('--parent_job_id')
205        args.remove('--parent_job_id')
206        # Remove the actual parent job id in command line arg.
207        del args[index]
208
209    # A dictionary of paths to replace in the command line. Key is the path to
210    # be replaced with the one in value.
211    paths_to_replace = {}
212    # Replace the control file path with the one in container.
213    if control:
214        container_control_filename = os.path.join(
215                lxc.CONTROL_TEMP_PATH, os.path.basename(control))
216        paths_to_replace[control] = container_control_filename
217    # Update result directory with the one in container.
218    container_result_dir = os.path.join(lxc.RESULT_DIR_FMT % _LXC_JOB_FOLDER)
219    if parser.options.results:
220        paths_to_replace[parser.options.results] = container_result_dir
221    args = [paths_to_replace.get(arg, arg) for arg in args]
222
223    # Apply --use-existing-results, results directory is aready created and
224    # mounted in container. Apply this arg to avoid exception being raised.
225    if not '--use-existing-results' in args:
226        args.append('--use-existing-results')
227
228    # Make sure autoserv running in container using a different pid file.
229    if not '--pidfile-label' in args:
230        args.extend(['--pidfile-label', 'container_autoserv'])
231
232    cmd_line = ' '.join(["'%s'" % arg if ' ' in arg else arg for arg in args])
233    logging.info('Run command in container: %s', cmd_line)
234    success = False
235    try:
236        test_container.attach_run(cmd_line)
237        success = True
238    except Exception as e:
239        # If the test run inside container fails without generating any log,
240        # write a message to status.log to help troubleshooting.
241        debug_files = os.listdir(os.path.join(results, 'debug'))
242        if not debug_files:
243            job.record('FAIL', None, None,
244                       'Failed to run test inside the container: %s. Check '
245                       'logs in ssp_logs folder for more details.' % e)
246        raise
247    finally:
248        metrics.Counter(
249            'chromeos/autotest/experimental/execute_job_in_ssp').increment(
250                fields={'success': success})
251        test_container.destroy()
252
253
254def correct_results_folder_permission(results):
255    """Make sure the results folder has the right permission settings.
256
257    For tests running with server-side packaging, the results folder has the
258    owner of root. This must be changed to the user running the autoserv
259    process, so parsing job can access the results folder.
260    TODO(dshi): crbug.com/459344 Remove this function when test container can be
261    unprivileged container.
262
263    @param results: Path to the results folder.
264
265    """
266    if not results:
267        return
268
269    utils.run('sudo -n chown -R %s "%s"' % (os.getuid(), results))
270    utils.run('sudo -n chgrp -R %s "%s"' % (os.getgid(), results))
271
272
273def _start_servod(machine):
274    """Try to start servod in moblab if it's not already running or running with
275    different board or port.
276
277    @param machine: Name of the dut used for test.
278    """
279    if not utils.is_moblab():
280        return
281
282    logging.debug('Trying to start servod.')
283    try:
284        afe = frontend.AFE()
285        board = server_utils.get_board_from_afe(machine, afe)
286        hosts = afe.get_hosts(hostname=machine)
287        servo_host = hosts[0].attributes.get('servo_host', None)
288        servo_port = hosts[0].attributes.get('servo_port', 9999)
289        if not servo_host in ['localhost', '127.0.0.1']:
290            logging.warn('Starting servod is aborted. The dut\'s servo_host '
291                         'attribute is not set to localhost.')
292            return
293    except (urllib.error.HTTPError, urllib.error.URLError):
294        # Ignore error if RPC failed to get board
295        logging.error('Failed to get board name from AFE. Start servod is '
296                      'aborted')
297        return
298
299    try:
300        pid = utils.run('pgrep servod').stdout
301        cmd_line = utils.run('ps -fp %s' % pid).stdout
302        if ('--board %s' % board in cmd_line and
303            '--port %s' % servo_port in cmd_line):
304            logging.debug('Servod is already running with given board and port.'
305                          ' There is no need to restart servod.')
306            return
307        logging.debug('Servod is running with different board or port. '
308                      'Stopping existing servod.')
309        utils.run('sudo stop servod')
310    except error.CmdError:
311        # servod is not running.
312        pass
313
314    try:
315        utils.run(START_SERVOD_CMD % (board, servo_port))
316        logging.debug('Servod is started')
317    except error.CmdError as e:
318        logging.error('Servod failed to be started, error: %s', e)
319
320
321def _control_path_on_disk(control_name):
322    """Find the control file corresponding to the given control name, on disk.
323
324    @param control_name: NAME attribute of the control file to fetch.
325    @return: Path to the control file.
326    """
327    cf_getter = suite.create_fs_getter(_AUTOTEST_ROOT)
328    control_name_predicate = suite.test_name_matches_pattern_predicate(
329            '^%s$' % control_name)
330    tests = suite.find_and_parse_tests(cf_getter, control_name_predicate)
331    if not tests:
332        raise error.AutoservError(
333                'Failed to find any control files with NAME %s' % control_name)
334    if len(tests) > 1:
335        logging.error('Found more than one control file with NAME %s: %s',
336                      control_name, [t.path for t in tests])
337        raise error.AutoservError(
338                'Found more than one control file with NAME %s' % control_name)
339    return tests[0].path
340
341
342def _stage_control_file(control_name, results_dir):
343    """Stage the control file to execute from local autotest checkout.
344
345    @param control_name: Name of the control file to stage.
346    @param results_dir: Results directory to stage the control file into.
347    @return: Absolute path to the staged control file.
348    """
349    control_path = _control_path_on_disk(control_name)
350    new_control = os.path.join(results_dir, _CONTROL_FILE_FROM_CONTROL_NAME)
351    shutil.copy2(control_path, new_control)
352    return new_control
353
354
355def run_autoserv(pid_file_manager, results, parser, ssp_url, use_ssp):
356    """Run server job with given options.
357
358    @param pid_file_manager: PidFileManager used to monitor the autoserv process
359    @param results: Folder to store results.
360    @param parser: Parser for the command line arguments.
361    @param ssp_url: Url to server-side package.
362    @param use_ssp: Set to True to run with server-side packaging.
363    """
364    # send stdin to /dev/null
365    dev_null = os.open(os.devnull, os.O_RDONLY)
366    os.dup2(dev_null, sys.stdin.fileno())
367    os.close(dev_null)
368
369    # Create separate process group if the process is not a process group
370    # leader. This allows autoserv process to keep running after the caller
371    # process (drone manager call) exits.
372    if os.getpid() != os.getpgid(0):
373        os.setsid()
374
375    # Container name is predefined so the container can be destroyed in
376    # handle_sigterm.
377    job_or_task_id = job_directories.get_job_id_or_task_id(
378            parser.options.results)
379    container_id = lxc.ContainerId(job_or_task_id, time.time(), os.getpid())
380
381    # Implement SIGTERM handler
382    def handle_sigterm(signum, frame):
383        logging.debug('Received SIGTERM')
384        if pid_file_manager:
385            pid_file_manager.close_file(1, signal.SIGTERM)
386        logging.debug('Finished writing to pid_file. Killing process.')
387
388        # Update results folder's file permission. This needs to be done ASAP
389        # before the parsing process tries to access the log.
390        if use_ssp and results:
391            correct_results_folder_permission(results)
392
393        # This sleep allows the pending output to be logged before the kill
394        # signal is sent.
395        time.sleep(.1)
396        if use_ssp:
397            logging.debug('Destroy container %s before aborting the autoserv '
398                          'process.', container_id)
399            try:
400                bucket = lxc.ContainerBucket(
401                        base_name=_ssp_base_image_name_or_default(
402                                parser.options))
403                container = bucket.get_container(container_id)
404                if container:
405                    container.destroy()
406                    logging.debug("Container %s destroyed.", container_id)
407                else:
408                    logging.debug('Container %s is not found.', container_id)
409                    bucket.scrub_container_location(container_id)
410            except:
411                # Handle any exception so the autoserv process can be aborted.
412                logging.exception('Failed to destroy container %s.',
413                                  container_id)
414            # Try to correct the result file permission again after the
415            # container is destroyed, as the container might have created some
416            # new files in the result folder.
417            if results:
418                correct_results_folder_permission(results)
419
420        os.killpg(os.getpgrp(), signal.SIGKILL)
421
422    # Set signal handler
423    signal.signal(signal.SIGTERM, handle_sigterm)
424
425    # faulthandler is only needed to debug in the Lab and is not avaliable to
426    # be imported in the chroot as part of VMTest, so Try-Except it.
427    try:
428        import faulthandler
429        faulthandler.register(signal.SIGTERM, all_threads=True, chain=True)
430        logging.debug('faulthandler registered on SIGTERM.')
431    except ImportError:
432        # exc_clear() doesn't exist (nor is needed) in python3
433        if six.PY2:
434            sys.exc_clear()
435
436    # Ignore SIGTTOU's generated by output from forked children.
437    signal.signal(signal.SIGTTOU, signal.SIG_IGN)
438
439    # If we received a SIGALARM, let's be loud about it.
440    signal.signal(signal.SIGALRM, log_alarm)
441
442    # Server side tests that call shell scripts often depend on $USER being set
443    # but depending on how you launch your autotest scheduler it may not be set.
444    os.environ['USER'] = getpass.getuser()
445
446    label = parser.options.label
447    group_name = parser.options.group_name
448    user = parser.options.user
449    client = parser.options.client
450    server = parser.options.server
451    verify = parser.options.verify
452    repair = parser.options.repair
453    cleanup = parser.options.cleanup
454    provision = parser.options.provision
455    reset = parser.options.reset
456    job_labels = parser.options.job_labels
457    no_tee = parser.options.no_tee
458    execution_tag = parser.options.execution_tag
459    ssh_user = parser.options.ssh_user
460    ssh_port = parser.options.ssh_port
461    ssh_pass = parser.options.ssh_pass
462    collect_crashinfo = parser.options.collect_crashinfo
463    control_filename = parser.options.control_filename
464    verify_job_repo_url = parser.options.verify_job_repo_url
465    skip_crash_collection = parser.options.skip_crash_collection
466    ssh_verbosity = int(parser.options.ssh_verbosity)
467    ssh_options = parser.options.ssh_options
468    no_use_packaging = parser.options.no_use_packaging
469    in_lab = bool(parser.options.lab)
470
471    # can't be both a client and a server side test
472    if client and server:
473        parser.parser.error("Can not specify a test as both server and client!")
474
475    if provision and client:
476        parser.parser.error("Cannot specify provisioning and client!")
477
478    is_special_task = (verify or repair or cleanup or collect_crashinfo or
479                       provision or reset)
480    use_client_trampoline = False
481    if parser.options.control_name:
482        if use_ssp:
483            # When use_ssp is True, autoserv will be re-executed inside a
484            # container preserving the --control-name argument. Control file
485            # will be staged inside the rexecuted autoserv.
486            control = None
487        else:
488            try:
489                control = _stage_control_file(parser.options.control_name,
490                                              results)
491            except error.AutoservError as e:
492                logging.info("Using client trampoline because of: %s", e)
493                control = parser.options.control_name
494                use_client_trampoline = True
495
496    elif parser.args:
497        control = parser.args[0]
498    else:
499        if not is_special_task:
500            parser.parser.error("Missing argument: control file")
501        control = None
502
503    if ssh_verbosity > 0:
504        # ssh_verbosity is an integer between 0 and 3, inclusive
505        ssh_verbosity_flag = '-' + 'v' * ssh_verbosity
506    else:
507        ssh_verbosity_flag = ''
508
509    machines = _get_machines(parser)
510    if group_name and len(machines) < 2:
511        parser.parser.error('-G %r may only be supplied with more than one '
512                            'machine.' % group_name)
513
514    logging.debug("Parser.args is %r", parser.args)
515    try:
516      logging.debug("Parser.options.args is %r", parser.options.args)
517    except AttributeError:
518      logging.debug("No Parser.options.args.")
519
520    try:
521      logging.debug("Parser.options is %r", parser.options)
522    except AttributeError:
523      logging.debug("No Parser.options.")
524    job_kwargs = {
525            'control': control,
526            'args': parser.args[1:],
527            'resultdir': results,
528            'label': label,
529            'user': user,
530            'machines': machines,
531            'machine_dict_list': server_job.get_machine_dicts(
532                    machine_names=machines,
533                    store_dir=os.path.join(results,
534                                           parser.options.host_info_subdir),
535                    in_lab=in_lab,
536                    use_shadow_store=not parser.options.local_only_host_info,
537                    host_attributes=parser.options.host_attributes,
538            ),
539            'client': client,
540            'ssh_user': ssh_user,
541            'ssh_port': ssh_port,
542            'ssh_pass': ssh_pass,
543            'ssh_verbosity_flag': ssh_verbosity_flag,
544            'ssh_options': ssh_options,
545            'group_name': group_name,
546            'tag': execution_tag,
547            'disable_sysinfo': parser.options.disable_sysinfo,
548            'in_lab': in_lab,
549            'use_client_trampoline': use_client_trampoline,
550            'sync_offload_dir': parser.options.sync_offload_dir,
551    }
552    if parser.options.parent_job_id:
553        job_kwargs['parent_job_id'] = int(parser.options.parent_job_id)
554    if control_filename:
555        job_kwargs['control_filename'] = control_filename
556    job = server_job.server_job(**job_kwargs)
557
558    job.logging.start_logging()
559
560    # perform checks
561    job.precheck()
562
563    # run the job
564    exit_code = 0
565    auto_start_servod = _CONFIG.get_config_value(
566            'AUTOSERV', 'auto_start_servod', type=bool, default=False)
567
568    if not utils.is_in_container():
569        # crbug.com/1054522 -- ts_mon setup is broken inside the SSP container
570        # due to a problem in the installed python packages.
571        # Trying to clean up an incorrectly initialized ts_mon state adds a 5
572        # second overhead in process teardown, so avoid setting up ts_mon
573        # entirely inside the SSP container.
574        site_utils.SetupTsMonGlobalState('autoserv', indirect=False,
575                                         short_lived=True)
576    try:
577        try:
578            if repair:
579                if auto_start_servod and len(machines) == 1:
580                    _start_servod(machines[0])
581                job.repair(job_labels)
582            elif verify:
583                job.verify(job_labels)
584            elif provision:
585                job.provision(job_labels)
586            elif reset:
587                job.reset(job_labels)
588            elif cleanup:
589                job.cleanup(job_labels)
590            else:
591                if auto_start_servod and len(machines) == 1:
592                    _start_servod(machines[0])
593                if use_ssp:
594                    try:
595                        _run_with_ssp(job, container_id, job_or_task_id,
596                                        results, parser, ssp_url, machines)
597                    finally:
598                        # Update the ownership of files in result folder.
599                        correct_results_folder_permission(results)
600                else:
601                    if collect_crashinfo:
602                        # Update the ownership of files in result folder. If the
603                        # job to collect crashinfo was running inside container
604                        # (SSP) and crashed before correcting folder permission,
605                        # the result folder might have wrong permission setting.
606                        try:
607                            correct_results_folder_permission(results)
608                        except:
609                            # Ignore any error as the user may not have root
610                            # permission to run sudo command.
611                            pass
612                    metric_name = ('chromeos/autotest/experimental/'
613                                   'autoserv_job_run_duration')
614                    f = {'in_container': utils.is_in_container(),
615                         'success': False}
616                    with metrics.SecondsTimer(metric_name, fields=f) as c:
617                        job.run(verify_job_repo_url=verify_job_repo_url,
618                                only_collect_crashinfo=collect_crashinfo,
619                                skip_crash_collection=skip_crash_collection,
620                                job_labels=job_labels,
621                                use_packaging=(not no_use_packaging))
622                        c['success'] = True
623
624        finally:
625            job.close()
626    except:
627        exit_code = 1
628        traceback.print_exc()
629    finally:
630        metrics.Flush()
631
632    sys.exit(exit_code)
633
634
635# Job breakdown statuses
636_hs = host_states.Status
637_qs = host_queue_entry_states.Status
638_status_list = [
639        _qs.QUEUED, _qs.RESETTING, _qs.VERIFYING,
640        _qs.PROVISIONING, _hs.REPAIRING, _qs.CLEANING,
641        _qs.RUNNING, _qs.GATHERING, _qs.PARSING]
642_JOB_OVERHEAD_STATUS = autotest_enum.AutotestEnum(*_status_list,
643                                                  string_values=True)
644
645
646def get_job_status(options):
647    """Returns the HQE Status for this run.
648
649    @param options: parser options.
650    """
651    s = _JOB_OVERHEAD_STATUS
652    task_mapping = {
653            'reset': s.RESETTING, 'verify': s.VERIFYING,
654            'provision': s.PROVISIONING, 'repair': s.REPAIRING,
655            'cleanup': s.CLEANING, 'collect_crashinfo': s.GATHERING}
656    match = [task for task in task_mapping if getattr(options, task, False)]
657    return task_mapping[match[0]] if match else s.RUNNING
658
659
660def _require_ssp_from_control(control_name):
661    """Read the value of REQUIRE_SSP from test control file.
662
663    This reads the control file from the prod checkout of autotest and uses that
664    to determine whether to even stage the SSP package on a devserver.
665
666    This means:
667    [1] Any change in REQUIRE_SSP directive in a test requires a prod-push to go
668    live.
669    [2] This function may find that the control file does not exist but the SSP
670    package may contain the test file. This function conservatively returns True
671    in that case.
672
673    This function is called very early in autoserv, before logging is setup.
674    """
675    if not control_name:
676        return True
677    try:
678        path = _control_path_on_disk(control_name)
679    except error.AutoservError as e:
680        sys.stderr.write("autoserv: Could not determine control file path,"
681                         " assuming we need SSP: %s\n" % e)
682        sys.stderr.flush()
683        return True
684    if not os.path.isfile(path):
685        return True
686    control = control_data.parse_control(path)
687    # There must be explicit directive in the control file to disable SSP.
688    if not control or control.require_ssp is None:
689        return True
690    return control.require_ssp
691
692
693def _ssp_base_image_name_or_default(options):
694    """Extract base image name from autoserv options or the global config."""
695    if options.ssp_base_image_name:
696        return options.ssp_base_image_name
697    return global_config.global_config.get_config_value('AUTOSERV',
698                                                        'container_base_name')
699
700
701def main():
702    start_time = datetime.datetime.now()
703    parser = autoserv_parser.autoserv_parser
704    parser.parse_args()
705
706    if len(sys.argv) == 1:
707        parser.parser.print_help()
708        sys.exit(1)
709
710    if parser.options.no_logging:
711        results = None
712    else:
713        results = parser.options.results
714        if not results:
715            results = 'results.' + time.strftime('%Y-%m-%d-%H.%M.%S')
716        results = os.path.abspath(results)
717        resultdir_exists = False
718        for filename in ('control.srv', 'status.log', '.autoserv_execute'):
719            if os.path.exists(os.path.join(results, filename)):
720                resultdir_exists = True
721        if not parser.options.use_existing_results and resultdir_exists:
722            error = "Error: results directory already exists: %s\n" % results
723            sys.stderr.write(error)
724            sys.exit(1)
725
726        # Now that we certified that there's no leftover results dir from
727        # previous jobs, lets create the result dir since the logging system
728        # needs to create the log file in there.
729        if not os.path.isdir(results):
730            os.makedirs(results)
731
732    if parser.options.require_ssp:
733        # This is currently only used for skylab (i.e., when --control-name is
734        # used).
735        use_ssp = _require_ssp_from_control(parser.options.control_name)
736    else:
737        use_ssp = False
738
739
740    if use_ssp:
741        log_dir = os.path.join(results, 'ssp_logs') if results else None
742        if log_dir and not os.path.exists(log_dir):
743            os.makedirs(log_dir)
744    else:
745        log_dir = results
746
747    logging_manager.configure_logging(
748            server_logging_config.ServerLoggingConfig(),
749            results_dir=log_dir,
750            use_console=not parser.options.no_tee,
751            verbose=parser.options.verbose,
752            no_console_prefix=parser.options.no_console_prefix)
753
754    logging.debug('autoserv is running in drone %s.', socket.gethostname())
755    logging.debug('autoserv environment: %r', os.environ)
756    logging.debug('autoserv command was: %s', ' '.join(sys.argv))
757    logging.debug('autoserv parsed options: %s', parser.options)
758
759    if use_ssp:
760        ssp_url = _stage_ssp(parser, results)
761    else:
762        ssp_url = None
763
764    if results:
765        logging.info("Results placed in %s" % results)
766
767        # wait until now to perform this check, so it get properly logged
768        if (parser.options.use_existing_results and not resultdir_exists and
769            not utils.is_in_container()):
770            logging.error("No existing results directory found: %s", results)
771            sys.exit(1)
772
773    if parser.options.write_pidfile and results:
774        pid_file_manager = pidfile.PidFileManager(parser.options.pidfile_label,
775                                                  results)
776        pid_file_manager.open_file()
777    else:
778        pid_file_manager = None
779
780    autotest.Autotest.set_install_in_tmpdir(
781        parser.options.install_in_tmpdir)
782
783    exit_code = 0
784    is_task = (parser.options.verify or parser.options.repair or
785               parser.options.provision or parser.options.reset or
786               parser.options.cleanup or parser.options.collect_crashinfo)
787
788    trace_labels = {
789            'job_id': job_directories.get_job_id_or_task_id(
790                    parser.options.results)
791    }
792    trace = cloud_trace.SpanStack(
793            labels=trace_labels,
794            global_context=parser.options.cloud_trace_context)
795    trace.enabled = parser.options.cloud_trace_context_enabled == 'True'
796    try:
797        try:
798            with trace.Span(get_job_status(parser.options)):
799                run_autoserv(pid_file_manager, results, parser, ssp_url,
800                             use_ssp)
801        except SystemExit as e:
802            exit_code = e.code
803            if exit_code:
804                logging.exception('Uncaught SystemExit with code %s', exit_code)
805        except Exception:
806            # If we don't know what happened, we'll classify it as
807            # an 'abort' and return 1.
808            logging.exception('Uncaught Exception, exit_code = 1.')
809            exit_code = 1
810    finally:
811        if pid_file_manager:
812            pid_file_manager.close_file(exit_code)
813    sys.exit(exit_code)
814
815
816if __name__ == '__main__':
817    main()
818