blob: c008a417c0a1d4088a625b969658dd55dc767fdb [file] [log] [blame]
mbligh6203ace2007-10-04 21:54:24 +00001#!/usr/bin/python -u
mbligh1ffd5dc2008-11-25 13:24:05 +00002# Copyright 2007-2008 Martin J. Bligh <mbligh@google.com>, Google Inc.
mbligh82648e52008-11-20 16:54:25 +00003# Released under the GPL v2
mblighdcd57a82007-07-11 23:06:47 +00004
mblighc8949b82007-07-23 16:33:58 +00005"""
Aviv Keshetde6bb192013-01-30 16:17:22 -08006Run a control file through the server side engine
mblighdcd57a82007-07-11 23:06:47 +00007"""
mbligh1ffd5dc2008-11-25 13:24:05 +00008
Fang Deng042c1472014-10-23 13:56:41 -07009import datetime
Paul Hobbs20cc72a2016-08-30 16:57:05 -070010import contextlib
Fang Deng042c1472014-10-23 13:56:41 -070011import getpass
12import logging
13import os
14import re
Prathmesh Prabhu46047362018-03-16 10:33:19 -070015import shutil
Fang Deng042c1472014-10-23 13:56:41 -070016import signal
Dan Shicf4d2032015-03-12 15:04:21 -070017import socket
Fang Deng042c1472014-10-23 13:56:41 -070018import sys
19import traceback
20import time
21import urllib2
mbligh1ffd5dc2008-11-25 13:24:05 +000022
mblighf5427bb2008-04-09 15:55:57 +000023import common
Dan Shi4f8c0242017-07-07 15:34:49 -070024from autotest_lib.client.bin.result_tools import utils as result_utils
25from autotest_lib.client.bin.result_tools import view as result_view
Dan Shia1ecd5c2013-06-06 11:21:31 -070026from autotest_lib.client.common_lib import control_data
Dan Shi32649b82015-08-29 20:53:36 -070027from autotest_lib.client.common_lib import error
Dan Shia1ecd5c2013-06-06 11:21:31 -070028from autotest_lib.client.common_lib import global_config
Allen Lif146e872017-08-15 18:24:31 -070029from autotest_lib.server import results_mocker
Prathmesh Prabhu46047362018-03-16 10:33:19 -070030from autotest_lib.server.cros.dynamic_suite import suite
Prathmesh Prabhua5eecda2016-11-23 16:48:40 -080031
Dan Shi5e2efb72017-02-07 11:40:23 -080032try:
33 from chromite.lib import metrics
Paul Hobbse9fd5572017-08-22 02:48:25 -070034 from chromite.lib import cloud_trace
Dan Shi5e2efb72017-02-07 11:40:23 -080035except ImportError:
Prathmesh Prabhud16c8012017-08-28 11:42:46 -070036 from autotest_lib.client.common_lib import utils as common_utils
37 metrics = common_utils.metrics_mock
Paul Hobbse9fd5572017-08-22 02:48:25 -070038 import mock
39 cloud_trace = mock.MagicMock()
Prathmesh Prabhua5eecda2016-11-23 16:48:40 -080040
Dan Shia06f3e22015-09-03 16:15:15 -070041_CONFIG = global_config.global_config
42
Dan Shia1ecd5c2013-06-06 11:21:31 -070043
Jakob Jueliche497b552014-09-23 19:11:59 -070044# Number of seconds to wait before returning if testing mode is enabled
Prashanth B6285f6a2014-05-08 18:01:27 -070045TESTING_MODE_SLEEP_SECS = 1
Jakob Jueliche497b552014-09-23 19:11:59 -070046
mbligh9ff89cd2009-09-03 20:28:17 +000047
Kevin Cheng9b6930f2016-07-20 14:57:15 -070048from autotest_lib.server import frontend
showard75cdfee2009-06-10 17:40:41 +000049from autotest_lib.server import server_logging_config
showard043c62a2009-06-10 19:48:57 +000050from autotest_lib.server import server_job, utils, autoserv_parser, autotest
Dan Shia1ecd5c2013-06-06 11:21:31 -070051from autotest_lib.server import utils as server_utils
Paul Hobbs20cc72a2016-08-30 16:57:05 -070052from autotest_lib.server import site_utils
Kevin Chengadc99f92016-07-20 08:21:58 -070053from autotest_lib.server.cros.dynamic_suite import frontend_wrappers
Dan Shicf4d2032015-03-12 15:04:21 -070054from autotest_lib.site_utils import job_directories
Fang Deng042c1472014-10-23 13:56:41 -070055from autotest_lib.site_utils import job_overhead
Dan Shicf4d2032015-03-12 15:04:21 -070056from autotest_lib.site_utils import lxc
Ben Kwa966db082017-06-05 14:17:23 -070057from autotest_lib.site_utils.lxc import utils as lxc_utils
showard75cdfee2009-06-10 17:40:41 +000058from autotest_lib.client.common_lib import pidfile, logging_manager
mbligh92c0fc22008-11-20 16:52:23 +000059
Paul Hobbs20cc72a2016-08-30 16:57:05 -070060
Dan Shicf4d2032015-03-12 15:04:21 -070061# Control segment to stage server-side package.
62STAGE_SERVER_SIDE_PACKAGE_CONTROL_FILE = server_job._control_segment_path(
63 'stage_server_side_package')
64
Dan Shia06f3e22015-09-03 16:15:15 -070065# Command line to start servod in a moblab.
66START_SERVOD_CMD = 'sudo start servod BOARD=%s PORT=%s'
67STOP_SERVOD_CMD = 'sudo stop servod'
68
Prathmesh Prabhu46047362018-03-16 10:33:19 -070069_AUTOTEST_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), '..'))
70_CONTROL_FILE_FROM_CONTROL_NAME = 'control.from_control_name'
71
Alex Millerf1af17e2013-01-09 22:50:32 -080072def log_alarm(signum, frame):
73 logging.error("Received SIGALARM. Ignoring and continuing on.")
Alex Miller0528d6f2013-01-11 10:49:48 -080074 sys.exit(1)
Alex Millerf1af17e2013-01-09 22:50:32 -080075
Dan Shicf4d2032015-03-12 15:04:21 -070076
77def _get_machines(parser):
78 """Get a list of machine names from command line arg -m or a file.
79
80 @param parser: Parser for the command line arguments.
81
82 @return: A list of machine names from command line arg -m or the
83 machines file specified in the command line arg -M.
84 """
85 if parser.options.machines:
86 machines = parser.options.machines.replace(',', ' ').strip().split()
87 else:
88 machines = []
89 machines_file = parser.options.machines_file
90 if machines_file:
91 machines = []
92 for m in open(machines_file, 'r').readlines():
93 # remove comments, spaces
94 m = re.sub('#.*', '', m).strip()
95 if m:
96 machines.append(m)
97 logging.debug('Read list of machines from file: %s', machines_file)
98 logging.debug('Machines: %s', ','.join(machines))
99
100 if machines:
101 for machine in machines:
102 if not machine or re.search('\s', machine):
103 parser.parser.error("Invalid machine: %s" % str(machine))
104 machines = list(set(machines))
105 machines.sort()
106 return machines
107
108
Prathmesh Prabhu588007d2017-06-15 00:31:31 -0700109def _stage_ssp(parser, resultsdir):
Dan Shicf4d2032015-03-12 15:04:21 -0700110 """Stage server-side package.
111
112 This function calls a control segment to stage server-side package based on
113 the job and autoserv command line option. The detail implementation could
114 be different for each host type. Currently, only CrosHost has
115 stage_server_side_package function defined.
116 The script returns None if no server-side package is available. However,
117 it may raise exception if it failed for reasons other than artifact (the
118 server-side package) not found.
119
120 @param parser: Command line arguments parser passed in the autoserv process.
Prathmesh Prabhu588007d2017-06-15 00:31:31 -0700121 @param resultsdir: Folder to store results. This could be different from
122 parser.options.results: parser.options.results can be set to None
123 for results to be stored in a temp folder. resultsdir can be None
124 for autoserv run requires no logging.
Dan Shicf4d2032015-03-12 15:04:21 -0700125
Dan Shi14de7622016-08-22 11:09:06 -0700126 @return: (ssp_url, error_msg), where
127 ssp_url is a url to the autotest server-side package. None if
128 server-side package is not supported.
129 error_msg is a string indicating the failures. None if server-
130 side package is staged successfully.
Dan Shicf4d2032015-03-12 15:04:21 -0700131 """
Kevin Chengadc99f92016-07-20 08:21:58 -0700132 machines_list = _get_machines(parser)
Prathmesh Prabhu7fc39c52018-03-21 14:08:30 -0700133 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 )
Kevin Chengadc99f92016-07-20 08:21:58 -0700140
Kevin Chengadc99f92016-07-20 08:21:58 -0700141 namespace = {'machines': machines_list,
Richard Barnette71854c72018-03-30 14:22:09 -0700142 'image': parser.options.test_source_build}
Dan Shicf4d2032015-03-12 15:04:21 -0700143 script_locals = {}
144 execfile(STAGE_SERVER_SIDE_PACKAGE_CONTROL_FILE, namespace, script_locals)
Dan Shi14de7622016-08-22 11:09:06 -0700145 return script_locals['ssp_url'], script_locals['error_msg']
Dan Shicf4d2032015-03-12 15:04:21 -0700146
147
Ben Kwabedacad2017-08-28 12:20:38 -0700148def _run_with_ssp(job, container_id, job_id, results, parser, ssp_url,
Dan Shi3be35af2016-08-25 23:22:40 -0700149 job_folder, machines):
Dan Shicf4d2032015-03-12 15:04:21 -0700150 """Run the server job with server-side packaging.
151
Dan Shi37befda2015-12-07 13:16:56 -0800152 @param job: The server job object.
Ben Kwabedacad2017-08-28 12:20:38 -0700153 @param container_id: ID of the container to run the test.
Dan Shicf4d2032015-03-12 15:04:21 -0700154 @param job_id: ID of the test job.
155 @param results: Folder to store results. This could be different from
156 parser.options.results:
157 parser.options.results can be set to None for results to be
158 stored in a temp folder.
159 results can be None for autoserv run requires no logging.
160 @param parser: Command line parser that contains the options.
161 @param ssp_url: url of the staged server-side package.
Dan Shiafa63872016-02-23 15:32:31 -0800162 @param job_folder: Name of the job result folder.
Dan Shi3be35af2016-08-25 23:22:40 -0700163 @param machines: A list of machines to run the test.
Dan Shicf4d2032015-03-12 15:04:21 -0700164 """
165 bucket = lxc.ContainerBucket()
166 control = (parser.args[0] if len(parser.args) > 0 and parser.args[0] != ''
167 else None)
Dan Shi37befda2015-12-07 13:16:56 -0800168 try:
Dan Shi3be35af2016-08-25 23:22:40 -0700169 dut_name = machines[0] if len(machines) >= 1 else None
Ben Kwabedacad2017-08-28 12:20:38 -0700170 test_container = bucket.setup_test(container_id, job_id, ssp_url,
Dan Shiafa63872016-02-23 15:32:31 -0800171 results, control=control,
Dan Shi3be35af2016-08-25 23:22:40 -0700172 job_folder=job_folder,
173 dut_name=dut_name)
Dan Shi37befda2015-12-07 13:16:56 -0800174 except Exception as e:
175 job.record('FAIL', None, None,
176 'Failed to setup container for test: %s. Check logs in '
177 'ssp_logs folder for more details.' % e)
178 raise
179
Dan Shicf4d2032015-03-12 15:04:21 -0700180 args = sys.argv[:]
181 args.remove('--require-ssp')
Dan Shi77b79a62015-07-29 16:22:05 -0700182 # --parent_job_id is only useful in autoserv running in host, not in
183 # container. Include this argument will cause test to fail for builds before
184 # CL 286265 was merged.
185 if '--parent_job_id' in args:
186 index = args.index('--parent_job_id')
187 args.remove('--parent_job_id')
188 # Remove the actual parent job id in command line arg.
189 del args[index]
Dan Shicf4d2032015-03-12 15:04:21 -0700190
191 # A dictionary of paths to replace in the command line. Key is the path to
192 # be replaced with the one in value.
193 paths_to_replace = {}
194 # Replace the control file path with the one in container.
195 if control:
196 container_control_filename = os.path.join(
197 lxc.CONTROL_TEMP_PATH, os.path.basename(control))
198 paths_to_replace[control] = container_control_filename
199 # Update result directory with the one in container.
Dan Shi65374e22016-09-15 16:14:05 -0700200 container_result_dir = os.path.join(lxc.RESULT_DIR_FMT % job_folder)
Dan Shicf4d2032015-03-12 15:04:21 -0700201 if parser.options.results:
Dan Shicf4d2032015-03-12 15:04:21 -0700202 paths_to_replace[parser.options.results] = container_result_dir
Dan Shicf4d2032015-03-12 15:04:21 -0700203 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
Dan Shid1f51232015-04-18 00:29:14 -0700214 cmd_line = ' '.join(["'%s'" % arg if ' ' in arg else arg for arg in args])
Dan Shicf4d2032015-03-12 15:04:21 -0700215 logging.info('Run command in container: %s', cmd_line)
Dan Shi37bee222015-04-13 15:46:47 -0700216 success = False
Dan Shicf4d2032015-03-12 15:04:21 -0700217 try:
218 test_container.attach_run(cmd_line)
Dan Shi37bee222015-04-13 15:46:47 -0700219 success = True
Dan Shi9d3454e2015-12-08 09:16:08 -0800220 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
Dan Shicf4d2032015-03-12 15:04:21 -0700229 finally:
Prathmesh Prabhua5eecda2016-11-23 16:48:40 -0800230 metrics.Counter(
231 'chromeos/autotest/experimental/execute_job_in_ssp').increment(
232 fields={'success': success})
Dan Shicf4d2032015-03-12 15:04:21 -0700233 test_container.destroy()
234
235
Dan Shi3f1b8a52015-04-21 11:11:06 -0700236def 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
Aviv Keshetc03de792017-07-18 14:24:31 -0700251 utils.run('sudo -n chown -R %s "%s"' % (os.getuid(), results))
252 utils.run('sudo -n chgrp -R %s "%s"' % (os.getgid(), results))
Dan Shi3f1b8a52015-04-21 11:11:06 -0700253
254
Dan Shia06f3e22015-09-03 16:15:15 -0700255def _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
Dan Shi1cded882015-09-23 16:52:26 -0700264 logging.debug('Trying to start servod.')
Dan Shia06f3e22015-09-03 16:15:15 -0700265 try:
Kevin Cheng9b6930f2016-07-20 14:57:15 -0700266 afe = frontend.AFE()
Dan Shia06f3e22015-09-03 16:15:15 -0700267 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']:
Dan Shi1cded882015-09-23 16:52:26 -0700272 logging.warn('Starting servod is aborted. The dut\'s servo_host '
273 'attribute is not set to localhost.')
Dan Shia06f3e22015-09-03 16:15:15 -0700274 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
Prathmesh Prabhu46047362018-03-16 10:33:19 -0700303def _control_path_on_disk(control_name):
304 """Find the control file corresponding to the given control name, on disk.
305
306 @param control_name: NAME attribute of the control file to fetch.
307 @return: Path to the control file.
308 """
309 cf_getter = suite.create_fs_getter(_AUTOTEST_ROOT)
310 control_name_predicate = suite.test_name_matches_pattern_predicate(
311 '^%s$' % control_name)
312 tests = suite.find_and_parse_tests(cf_getter, control_name_predicate)
313 if not tests:
314 raise error.AutoservError(
315 'Failed to find any control files with NAME %s' % control_name)
316 if len(tests) > 1:
317 logging.error('Found more than one control file with NAME %s: %s',
318 control_name, [t.path for t in tests])
319 raise error.AutoservError(
320 'Found more than one control file with NAME %s' % control_name)
321 return tests[0].path
322
323
324def _stage_control_file(parser, results_dir):
325 """Stage the control file to execute, returning the path to staged file.
326
327 @param parser: Parser for autoserv options.
328 @param results_dir: Results directory to stage the control file into.
329 @return: Absolute path to the staged control file.
330 """
331 # TODO(pprabhu) This function currently always stages the control file from
332 # the local filesystem. This means that both
333 # parser.options.test_source_build and parser.options.image are ignored.
334 # Support will be added once skylab gains support to run SSP tests.
335 control_path = _control_path_on_disk(parser.options.control_name)
336 new_control = os.path.join(results_dir, _CONTROL_FILE_FROM_CONTROL_NAME)
337 shutil.copy2(control_path, new_control)
338 return new_control
339
340
341def _tweak_arguments_for_control_file(parser, control):
342 """Tweak parser arguments to pass in control.
343
344 autoserv running within an SSP container may not support the --test-name
345 argument. We also do not want to duplicate the effort and logic to obtain
346 the right control file outside and inside the SSP container. Instead, we
347 tweak the parser commandline in order to pass in the given control file.
348 """
349 # control_name overrides the control argument, so unset it to force the
350 # autoserv re-execution to use the control file set here.
351 parser.control_name = None
352 if parser.args:
353 parser.args[0] = control
354 else:
355 parser.args.append(control)
356
357
Dan Shic68fefb2015-04-07 10:10:52 -0700358def run_autoserv(pid_file_manager, results, parser, ssp_url, use_ssp):
Dan Shicf4d2032015-03-12 15:04:21 -0700359 """Run server job with given options.
360
361 @param pid_file_manager: PidFileManager used to monitor the autoserv process
362 @param results: Folder to store results.
363 @param parser: Parser for the command line arguments.
364 @param ssp_url: Url to server-side package.
Dan Shic68fefb2015-04-07 10:10:52 -0700365 @param use_ssp: Set to True to run with server-side packaging.
Dan Shicf4d2032015-03-12 15:04:21 -0700366 """
Dan Shiec1d47d2015-02-13 11:38:13 -0800367 if parser.options.warn_no_ssp:
Dan Shic68fefb2015-04-07 10:10:52 -0700368 # Post a warning in the log.
Dan Shiec1d47d2015-02-13 11:38:13 -0800369 logging.warn('Autoserv is required to run with server-side packaging. '
370 'However, no drone is found to support server-side '
371 'packaging. The test will be executed in a drone without '
372 'server-side packaging supported.')
373
jadmanski0afbb632008-06-06 21:10:57 +0000374 # send stdin to /dev/null
375 dev_null = os.open(os.devnull, os.O_RDONLY)
376 os.dup2(dev_null, sys.stdin.fileno())
377 os.close(dev_null)
mblighdbf37612007-11-24 19:38:11 +0000378
Dan Shie8aeb662016-06-30 11:22:03 -0700379 # Create separate process group if the process is not a process group
380 # leader. This allows autoserv process to keep running after the caller
381 # process (drone manager call) exits.
382 if os.getpid() != os.getpgid(0):
383 os.setsid()
mbligh1d42d4e2007-11-05 22:42:00 +0000384
Dan Shicf4d2032015-03-12 15:04:21 -0700385 # Container name is predefined so the container can be destroyed in
386 # handle_sigterm.
387 job_or_task_id = job_directories.get_job_id_or_task_id(
388 parser.options.results)
Ben Kwabedacad2017-08-28 12:20:38 -0700389 container_id = lxc.ContainerId(job_or_task_id, time.time(), os.getpid())
Dan Shiafa63872016-02-23 15:32:31 -0800390 job_folder = job_directories.get_job_folder_name(parser.options.results)
Dan Shicf4d2032015-03-12 15:04:21 -0700391
jadmanski0afbb632008-06-06 21:10:57 +0000392 # Implement SIGTERM handler
mblighc2299562009-07-02 19:00:36 +0000393 def handle_sigterm(signum, frame):
Simran Basi9d9b7292013-10-16 16:44:07 -0700394 logging.debug('Received SIGTERM')
mblighff7d61f2008-12-22 14:53:35 +0000395 if pid_file_manager:
396 pid_file_manager.close_file(1, signal.SIGTERM)
Simran Basi49e21e62013-10-17 12:40:33 -0700397 logging.debug('Finished writing to pid_file. Killing process.')
Dan Shi3f1b8a52015-04-21 11:11:06 -0700398
399 # Update results folder's file permission. This needs to be done ASAP
400 # before the parsing process tries to access the log.
401 if use_ssp and results:
402 correct_results_folder_permission(results)
403
Simran Basid6b83772014-01-06 16:31:30 -0800404 # TODO (sbasi) - remove the time.sleep when crbug.com/302815 is solved.
405 # This sleep allows the pending output to be logged before the kill
406 # signal is sent.
407 time.sleep(.1)
Dan Shic68fefb2015-04-07 10:10:52 -0700408 if use_ssp:
Dan Shicf4d2032015-03-12 15:04:21 -0700409 logging.debug('Destroy container %s before aborting the autoserv '
Ben Kwabedacad2017-08-28 12:20:38 -0700410 'process.', container_id)
Dan Shicf4d2032015-03-12 15:04:21 -0700411 try:
412 bucket = lxc.ContainerBucket()
Ben Kwabedacad2017-08-28 12:20:38 -0700413 container = bucket.get_container(container_id)
Dan Shicf4d2032015-03-12 15:04:21 -0700414 if container:
415 container.destroy()
416 else:
Ben Kwabedacad2017-08-28 12:20:38 -0700417 logging.debug('Container %s is not found.', container_id)
Dan Shicf4d2032015-03-12 15:04:21 -0700418 except:
419 # Handle any exception so the autoserv process can be aborted.
Dan Shi65374e22016-09-15 16:14:05 -0700420 logging.exception('Failed to destroy container %s.',
Ben Kwabedacad2017-08-28 12:20:38 -0700421 container_id)
Dan Shie4a4f9f2015-07-20 09:00:25 -0700422 # Try to correct the result file permission again after the
423 # container is destroyed, as the container might have created some
424 # new files in the result folder.
425 if results:
426 correct_results_folder_permission(results)
Dan Shicf4d2032015-03-12 15:04:21 -0700427
jadmanski0afbb632008-06-06 21:10:57 +0000428 os.killpg(os.getpgrp(), signal.SIGKILL)
mblighfaf0cd42007-11-19 16:00:24 +0000429
jadmanski0afbb632008-06-06 21:10:57 +0000430 # Set signal handler
mblighc2299562009-07-02 19:00:36 +0000431 signal.signal(signal.SIGTERM, handle_sigterm)
mbligha46678d2008-05-01 20:00:01 +0000432
Simran Basid6b83772014-01-06 16:31:30 -0800433 # faulthandler is only needed to debug in the Lab and is not avaliable to
434 # be imported in the chroot as part of VMTest, so Try-Except it.
435 try:
436 import faulthandler
437 faulthandler.register(signal.SIGTERM, all_threads=True, chain=True)
438 logging.debug('faulthandler registered on SIGTERM.')
439 except ImportError:
Christopher Grant4beca022015-06-16 15:14:47 -0400440 sys.exc_clear()
Simran Basid6b83772014-01-06 16:31:30 -0800441
David Rochberg8a60d1e2011-02-01 14:22:07 -0500442 # Ignore SIGTTOU's generated by output from forked children.
443 signal.signal(signal.SIGTTOU, signal.SIG_IGN)
444
Alex Millerf1af17e2013-01-09 22:50:32 -0800445 # If we received a SIGALARM, let's be loud about it.
446 signal.signal(signal.SIGALRM, log_alarm)
447
mbligha5f5e542009-12-30 16:57:49 +0000448 # Server side tests that call shell scripts often depend on $USER being set
449 # but depending on how you launch your autotest scheduler it may not be set.
450 os.environ['USER'] = getpass.getuser()
451
mblighb2bea302008-07-24 20:25:57 +0000452 label = parser.options.label
mbligh374f3412009-05-13 21:29:45 +0000453 group_name = parser.options.group_name
mblighb2bea302008-07-24 20:25:57 +0000454 user = parser.options.user
455 client = parser.options.client
456 server = parser.options.server
mblighb2bea302008-07-24 20:25:57 +0000457 verify = parser.options.verify
458 repair = parser.options.repair
showard45ae8192008-11-05 19:32:53 +0000459 cleanup = parser.options.cleanup
Alex Millercb79ba72013-05-29 14:43:00 -0700460 provision = parser.options.provision
Dan Shi07e09af2013-04-12 09:31:29 -0700461 reset = parser.options.reset
Alex Miller667b5f22014-02-28 15:33:39 -0800462 job_labels = parser.options.job_labels
mblighb2bea302008-07-24 20:25:57 +0000463 no_tee = parser.options.no_tee
mblighe7d9c602009-07-02 19:02:33 +0000464 execution_tag = parser.options.execution_tag
jadmanski0afbb632008-06-06 21:10:57 +0000465 ssh_user = parser.options.ssh_user
466 ssh_port = parser.options.ssh_port
467 ssh_pass = parser.options.ssh_pass
jadmanskidef0c3c2009-03-25 20:07:10 +0000468 collect_crashinfo = parser.options.collect_crashinfo
mblighe0cbc912010-03-11 18:03:07 +0000469 control_filename = parser.options.control_filename
Scott Zawalski91493c82013-01-25 16:15:20 -0500470 test_retry = parser.options.test_retry
beepscb6f1e22013-06-28 19:14:10 -0700471 verify_job_repo_url = parser.options.verify_job_repo_url
Christopher Wileyf594c5e2013-07-03 18:25:30 -0700472 skip_crash_collection = parser.options.skip_crash_collection
Aviv Keshet18ee3142013-08-12 15:01:51 -0700473 ssh_verbosity = int(parser.options.ssh_verbosity)
Fang Deng6cc20de2013-09-06 15:47:32 -0700474 ssh_options = parser.options.ssh_options
Dan Shib669cbd2013-09-13 11:17:17 -0700475 no_use_packaging = parser.options.no_use_packaging
Simran Basi1bf60eb2015-12-01 16:39:29 -0800476 in_lab = bool(parser.options.lab)
mbligha46678d2008-05-01 20:00:01 +0000477
mblighb2bea302008-07-24 20:25:57 +0000478 # can't be both a client and a server side test
479 if client and server:
Eric Li861b2d52011-02-04 14:50:35 -0800480 parser.parser.error("Can not specify a test as both server and client!")
mblighb2bea302008-07-24 20:25:57 +0000481
Alex Millercb79ba72013-05-29 14:43:00 -0700482 if provision and client:
483 parser.parser.error("Cannot specify provisioning and client!")
484
485 is_special_task = (verify or repair or cleanup or collect_crashinfo or
Dan Shi07e09af2013-04-12 09:31:29 -0700486 provision or reset)
Prathmesh Prabhu46047362018-03-16 10:33:19 -0700487 if parser.options.control_name:
488 control = _stage_control_file(parser, results)
489 _tweak_arguments_for_control_file(parser, control)
490 elif parser.args:
491 control = parser.args[0]
492 else:
493 # Special tasks do not have any control file at all.
494 control = None
495
496 if not any([is_special_task, control]):
Eric Li861b2d52011-02-04 14:50:35 -0800497 parser.parser.error("Missing argument: control file")
mbligha46678d2008-05-01 20:00:01 +0000498
Aviv Keshet18ee3142013-08-12 15:01:51 -0700499 if ssh_verbosity > 0:
500 # ssh_verbosity is an integer between 0 and 3, inclusive
501 ssh_verbosity_flag = '-' + 'v' * ssh_verbosity
Fang Dengd1c2b732013-08-20 12:59:46 -0700502 else:
503 ssh_verbosity_flag = ''
Aviv Keshet18ee3142013-08-12 15:01:51 -0700504
Dan Shicf4d2032015-03-12 15:04:21 -0700505 machines = _get_machines(parser)
mbligh374f3412009-05-13 21:29:45 +0000506 if group_name and len(machines) < 2:
Dan Shicf4d2032015-03-12 15:04:21 -0700507 parser.parser.error('-G %r may only be supplied with more than one '
508 'machine.' % group_name)
mbligh374f3412009-05-13 21:29:45 +0000509
Prathmesh Prabhu7fc39c52018-03-21 14:08:30 -0700510 job_kwargs = {
511 'control': control,
512 'args': parser.args[1:],
513 'resultdir': results,
514 'label': label,
515 'user': user,
516 'machines': machines,
517 'machine_dict_list': server_job.get_machine_dicts(
518 machine_names=machines,
519 store_dir=os.path.join(results,
520 parser.options.host_info_subdir),
521 in_lab=in_lab,
522 use_shadow_store=not parser.options.local_only_host_info,
523 host_attributes=parser.options.host_attributes,
524 ),
525 'client': client,
Prathmesh Prabhu7fc39c52018-03-21 14:08:30 -0700526 'ssh_user': ssh_user,
527 'ssh_port': ssh_port,
528 'ssh_pass': ssh_pass,
529 'ssh_verbosity_flag': ssh_verbosity_flag,
530 'ssh_options': ssh_options,
531 'test_retry': test_retry,
532 'group_name': group_name,
533 'tag': execution_tag,
534 'disable_sysinfo': parser.options.disable_sysinfo,
535 'in_lab': in_lab,
536 }
Dan Shi70647ca2015-07-16 22:52:35 -0700537 if parser.options.parent_job_id:
Prathmesh Prabhu7fc39c52018-03-21 14:08:30 -0700538 job_kwargs['parent_job_id'] = int(parser.options.parent_job_id)
mblighe0cbc912010-03-11 18:03:07 +0000539 if control_filename:
Prathmesh Prabhu7fc39c52018-03-21 14:08:30 -0700540 job_kwargs['control_filename'] = control_filename
541 job = server_job.server_job(**job_kwargs)
Dan Shicf4d2032015-03-12 15:04:21 -0700542
showard75cdfee2009-06-10 17:40:41 +0000543 job.logging.start_logging()
mbligha46678d2008-05-01 20:00:01 +0000544
mbligh161fe6f2008-06-19 16:26:04 +0000545 # perform checks
546 job.precheck()
547
jadmanski0afbb632008-06-06 21:10:57 +0000548 # run the job
549 exit_code = 0
Dan Shic1b8bdd2015-09-14 23:11:24 -0700550 auto_start_servod = _CONFIG.get_config_value(
551 'AUTOSERV', 'auto_start_servod', type=bool, default=False)
Paul Hobbs20cc72a2016-08-30 16:57:05 -0700552
Prathmesh Prabhu27bba962017-01-24 15:13:07 -0800553 site_utils.SetupTsMonGlobalState('autoserv', indirect=False,
Paul Hobbs66251f62017-08-22 02:31:07 -0700554 auto_flush=False, short_lived=True)
jadmanski0afbb632008-06-06 21:10:57 +0000555 try:
Prathmesh Prabhu27bba962017-01-24 15:13:07 -0800556 try:
557 if repair:
558 if auto_start_servod and len(machines) == 1:
559 _start_servod(machines[0])
560 job.repair(job_labels)
561 elif verify:
562 job.verify(job_labels)
563 elif provision:
564 job.provision(job_labels)
565 elif reset:
566 job.reset(job_labels)
567 elif cleanup:
568 job.cleanup(job_labels)
569 else:
570 if auto_start_servod and len(machines) == 1:
571 _start_servod(machines[0])
572 if use_ssp:
573 try:
Ben Kwabedacad2017-08-28 12:20:38 -0700574 _run_with_ssp(job, container_id, job_or_task_id,
Prathmesh Prabhu27bba962017-01-24 15:13:07 -0800575 results, parser, ssp_url, job_folder,
576 machines)
577 finally:
578 # Update the ownership of files in result folder.
579 correct_results_folder_permission(results)
Dan Shicf4d2032015-03-12 15:04:21 -0700580 else:
Prathmesh Prabhu27bba962017-01-24 15:13:07 -0800581 if collect_crashinfo:
582 # Update the ownership of files in result folder. If the
583 # job to collect crashinfo was running inside container
584 # (SSP) and crashed before correcting folder permission,
585 # the result folder might have wrong permission setting.
Dan Shiafa63872016-02-23 15:32:31 -0800586 try:
587 correct_results_folder_permission(results)
Prathmesh Prabhu27bba962017-01-24 15:13:07 -0800588 except:
589 # Ignore any error as the user may not have root
590 # permission to run sudo command.
591 pass
Aviv Keshet92bf7b62017-02-13 15:34:03 -0800592 metric_name = ('chromeos/autotest/experimental/'
593 'autoserv_job_run_duration')
594 f = {'in_container': utils.is_in_container(),
595 'success': False}
596 with metrics.SecondsTimer(metric_name, fields=f) as c:
Richard Barnette71854c72018-03-30 14:22:09 -0700597 job.run(verify_job_repo_url=verify_job_repo_url,
Aviv Keshet92bf7b62017-02-13 15:34:03 -0800598 only_collect_crashinfo=collect_crashinfo,
599 skip_crash_collection=skip_crash_collection,
600 job_labels=job_labels,
601 use_packaging=(not no_use_packaging))
602 c['success'] = True
603
Prathmesh Prabhu27bba962017-01-24 15:13:07 -0800604 finally:
Hidehiko Abe06893302017-06-24 07:32:38 +0900605 job.close()
Dan Shiffd5b822017-07-14 11:16:23 -0700606 # Special task doesn't run parse, so result summary needs to be
607 # built here.
608 if results and (repair or verify or reset or cleanup or provision):
Dan Shi4f8c0242017-07-07 15:34:49 -0700609 # Throttle the result on the server side.
610 try:
611 result_utils.execute(
612 results, control_data.DEFAULT_MAX_RESULT_SIZE_KB)
613 except:
614 logging.exception(
615 'Non-critical failure: Failed to throttle results '
616 'in directory %s.', results)
617 # Build result view and report metrics for result sizes.
Dan Shiffd5b822017-07-14 11:16:23 -0700618 site_utils.collect_result_sizes(results)
jadmanski0afbb632008-06-06 21:10:57 +0000619 except:
jadmanski27b37ea2008-10-29 23:54:31 +0000620 exit_code = 1
jadmanski0afbb632008-06-06 21:10:57 +0000621 traceback.print_exc()
Prathmesh Prabhu27bba962017-01-24 15:13:07 -0800622 finally:
623 metrics.Flush()
mbligha46678d2008-05-01 20:00:01 +0000624
jadmanski27b37ea2008-10-29 23:54:31 +0000625 sys.exit(exit_code)
mbligha46678d2008-05-01 20:00:01 +0000626
627
Paul Hobbs68d98592017-08-22 02:22:49 -0700628def record_autoserv(options, start_time):
Fang Deng042c1472014-10-23 13:56:41 -0700629 """Record autoserv end-to-end time in metadata db.
630
631 @param options: parser options.
Paul Hobbs68d98592017-08-22 02:22:49 -0700632 @param start_time: When autoserv started
Fang Deng042c1472014-10-23 13:56:41 -0700633 """
634 # Get machine hostname
635 machines = options.machines.replace(
636 ',', ' ').strip().split() if options.machines else []
637 num_machines = len(machines)
638 if num_machines > 1:
639 # Skip the case where atomic group is used.
640 return
641 elif num_machines == 0:
642 machines.append('hostless')
643
644 # Determine the status that will be reported.
Paul Hobbs68d98592017-08-22 02:22:49 -0700645 status = get_job_status(options)
646 is_special_task = status not in [
Paul Hobbse9fd5572017-08-22 02:48:25 -0700647 job_overhead.STATUS.RUNNING, job_overhead.STATUS.GATHERING]
Paul Hobbs68d98592017-08-22 02:22:49 -0700648 job_or_task_id = job_directories.get_job_id_or_task_id(options.results)
649 duration_secs = (datetime.datetime.now() - start_time).total_seconds()
650 job_overhead.record_state_duration(
651 job_or_task_id, machines[0], status, duration_secs,
652 is_special_task=is_special_task)
653
654
655def get_job_status(options):
656 """Returns the HQE Status for this run.
657
658 @param options: parser options.
659 """
Fang Deng042c1472014-10-23 13:56:41 -0700660 s = job_overhead.STATUS
661 task_mapping = {
662 'reset': s.RESETTING, 'verify': s.VERIFYING,
663 'provision': s.PROVISIONING, 'repair': s.REPAIRING,
664 'cleanup': s.CLEANING, 'collect_crashinfo': s.GATHERING}
Paul Hobbs68d98592017-08-22 02:22:49 -0700665 match = [task for task in task_mapping if getattr(options, task, False)]
666 return task_mapping[match[0]] if match else s.RUNNING
Fang Deng042c1472014-10-23 13:56:41 -0700667
668
mbligha46678d2008-05-01 20:00:01 +0000669def main():
Fang Deng042c1472014-10-23 13:56:41 -0700670 start_time = datetime.datetime.now()
jadmanski0afbb632008-06-06 21:10:57 +0000671 # grab the parser
672 parser = autoserv_parser.autoserv_parser
mbligha5cb4062009-02-17 15:53:39 +0000673 parser.parse_args()
mbligha46678d2008-05-01 20:00:01 +0000674
jadmanski0afbb632008-06-06 21:10:57 +0000675 if len(sys.argv) == 1:
676 parser.parser.print_help()
677 sys.exit(1)
mbligha6f13082008-06-05 23:53:46 +0000678
showard75cdfee2009-06-10 17:40:41 +0000679 if parser.options.no_logging:
680 results = None
681 else:
682 results = parser.options.results
mbligh80e1eba2008-11-19 00:26:18 +0000683 if not results:
684 results = 'results.' + time.strftime('%Y-%m-%d-%H.%M.%S')
Dan Shi14de7622016-08-22 11:09:06 -0700685 results = os.path.abspath(results)
showard566d3c02010-01-12 18:57:01 +0000686 resultdir_exists = False
687 for filename in ('control.srv', 'status.log', '.autoserv_execute'):
688 if os.path.exists(os.path.join(results, filename)):
689 resultdir_exists = True
mbligh4608b002010-01-05 18:22:35 +0000690 if not parser.options.use_existing_results and resultdir_exists:
mbligh80e1eba2008-11-19 00:26:18 +0000691 error = "Error: results directory already exists: %s\n" % results
692 sys.stderr.write(error)
693 sys.exit(1)
mbligha788dc42009-03-26 21:10:16 +0000694
695 # Now that we certified that there's no leftover results dir from
696 # previous jobs, lets create the result dir since the logging system
697 # needs to create the log file in there.
698 if not os.path.isdir(results):
699 os.makedirs(results)
showard75cdfee2009-06-10 17:40:41 +0000700
Prathmesh Prabhu7ae68ae2017-06-15 00:29:31 -0700701 # If the job requires to run with server-side package, try to stage server-
702 # side package first. If that fails with error that autotest server package
703 # does not exist, fall back to run the job without using server-side
704 # packaging. If option warn_no_ssp is specified, that means autoserv is
705 # running in a drone does not support SSP, thus no need to stage server-side
706 # package.
707 ssp_url = None
708 ssp_url_warning = False
709 if (not parser.options.warn_no_ssp and parser.options.require_ssp):
Prathmesh Prabhu588007d2017-06-15 00:31:31 -0700710 ssp_url, ssp_error_msg = _stage_ssp(parser, results)
Prathmesh Prabhu7ae68ae2017-06-15 00:29:31 -0700711 # The build does not have autotest server package. Fall back to not
712 # to use server-side package. Logging is postponed until logging being
713 # set up.
714 ssp_url_warning = not ssp_url
715
Dan Shic68fefb2015-04-07 10:10:52 -0700716 # Server-side packaging will only be used if it's required and the package
717 # is available. If warn_no_ssp is specified, it means that autoserv is
718 # running in a drone does not have SSP supported and a warning will be logs.
719 # Therefore, it should not run with SSP.
720 use_ssp = (not parser.options.warn_no_ssp and parser.options.require_ssp
721 and ssp_url)
722 if use_ssp:
Dan Shie28de552015-05-06 16:51:58 -0700723 log_dir = os.path.join(results, 'ssp_logs') if results else None
Dan Shicf4d2032015-03-12 15:04:21 -0700724 if log_dir and not os.path.exists(log_dir):
725 os.makedirs(log_dir)
726 else:
727 log_dir = results
Dan Shi3f1b8a52015-04-21 11:11:06 -0700728
showard75cdfee2009-06-10 17:40:41 +0000729 logging_manager.configure_logging(
Dan Shicf4d2032015-03-12 15:04:21 -0700730 server_logging_config.ServerLoggingConfig(),
731 results_dir=log_dir,
showard10d84172009-06-18 23:16:50 +0000732 use_console=not parser.options.no_tee,
733 verbose=parser.options.verbose,
734 no_console_prefix=parser.options.no_console_prefix)
Dan Shicf4d2032015-03-12 15:04:21 -0700735
Dan Shi0b754c52015-04-20 14:20:38 -0700736 if ssp_url_warning:
737 logging.warn(
738 'Autoserv is required to run with server-side packaging. '
Dan Shi6bfbdb62017-09-25 13:33:53 -0700739 'However, no server-side package can be staged based on '
Richard Barnette71854c72018-03-30 14:22:09 -0700740 '`--test_source_build`, host attribute job_repo_url or host '
741 'OS version label. It could be that the build to test is '
742 'older than the minimum version that supports server-side '
743 'packaging, or no devserver can be found to stage server-side '
744 'package. The test will be executed without using server-side '
745 'packaging. Following is the detailed error:\n%s',
746 ssp_error_msg)
Dan Shi0b754c52015-04-20 14:20:38 -0700747
showard75cdfee2009-06-10 17:40:41 +0000748 if results:
mbligha788dc42009-03-26 21:10:16 +0000749 logging.info("Results placed in %s" % results)
mbligh10717632008-11-19 00:21:57 +0000750
mbligh4608b002010-01-05 18:22:35 +0000751 # wait until now to perform this check, so it get properly logged
Dan Shicf4d2032015-03-12 15:04:21 -0700752 if (parser.options.use_existing_results and not resultdir_exists and
Dan Shiff78f112015-06-12 13:34:02 -0700753 not utils.is_in_container()):
mbligh4608b002010-01-05 18:22:35 +0000754 logging.error("No existing results directory found: %s", results)
755 sys.exit(1)
756
Dan Shicf4d2032015-03-12 15:04:21 -0700757 logging.debug('autoserv is running in drone %s.', socket.gethostname())
Aviv Keshet5c40ec62013-08-20 12:11:12 -0700758 logging.debug('autoserv command was: %s', ' '.join(sys.argv))
Prathmesh Prabhu7fc39c52018-03-21 14:08:30 -0700759 logging.debug('autoserv parsed options: %s', parser.options)
mbligh4608b002010-01-05 18:22:35 +0000760
Dan Shicf4d2032015-03-12 15:04:21 -0700761 if parser.options.write_pidfile and results:
mbligh4608b002010-01-05 18:22:35 +0000762 pid_file_manager = pidfile.PidFileManager(parser.options.pidfile_label,
763 results)
jadmanskid5ab8c52008-12-03 16:27:07 +0000764 pid_file_manager.open_file()
mblighff7d61f2008-12-22 14:53:35 +0000765 else:
766 pid_file_manager = None
mbligha46678d2008-05-01 20:00:01 +0000767
Allen Lid5abdab2017-02-07 16:03:43 -0800768 autotest.Autotest.set_install_in_tmpdir(
jadmanskif22fea82008-11-26 20:57:07 +0000769 parser.options.install_in_tmpdir)
770
jadmanski0afbb632008-06-06 21:10:57 +0000771 exit_code = 0
Allen Lif146e872017-08-15 18:24:31 -0700772 # TODO(beeps): Extend this to cover different failure modes.
773 # Testing exceptions are matched against labels sent to autoserv. Eg,
774 # to allow only the hostless job to run, specify
775 # testing_exceptions: test_suite in the shadow_config. To allow both
776 # the hostless job and dummy_Pass to run, specify
777 # testing_exceptions: test_suite,dummy_Pass. You can figure out
778 # what label autoserv is invoked with by looking through the logs of a test
779 # for the autoserv command's -l option.
780 testing_exceptions = _CONFIG.get_config_value(
781 'AUTOSERV', 'testing_exceptions', type=list, default=[])
782 test_mode = _CONFIG.get_config_value(
783 'AUTOSERV', 'testing_mode', type=bool, default=False)
784 test_mode = (results_mocker and test_mode and not
785 any([ex in parser.options.label
786 for ex in testing_exceptions]))
787 is_task = (parser.options.verify or parser.options.repair or
788 parser.options.provision or parser.options.reset or
789 parser.options.cleanup or parser.options.collect_crashinfo)
Paul Hobbse9fd5572017-08-22 02:48:25 -0700790
791 trace_labels = {
792 'job_id': job_directories.get_job_id_or_task_id(
793 parser.options.results)
794 }
795 trace = cloud_trace.SpanStack(
796 labels=trace_labels,
797 global_context=parser.options.cloud_trace_context)
798 trace.enabled = parser.options.cloud_trace_context_enabled == 'True'
jadmanski0afbb632008-06-06 21:10:57 +0000799 try:
800 try:
Allen Lif146e872017-08-15 18:24:31 -0700801 if test_mode:
802 # The parser doesn't run on tasks anyway, so we can just return
803 # happy signals without faking results.
804 if not is_task:
805 machine = parser.options.results.split('/')[-1]
806
807 # TODO(beeps): The proper way to do this would be to
808 # refactor job creation so we can invoke job.record
809 # directly. To do that one needs to pipe the test_name
810 # through run_autoserv and bail just before invoking
811 # the server job. See the comment in
812 # puppylab/results_mocker for more context.
813 results_mocker.ResultsMocker(
814 'unknown-test', parser.options.results, machine
815 ).mock_results()
816 return
817 else:
Paul Hobbse9fd5572017-08-22 02:48:25 -0700818 with trace.Span(get_job_status(parser.options)):
819 run_autoserv(pid_file_manager, results, parser, ssp_url,
820 use_ssp)
Aviv Keshet5c40ec62013-08-20 12:11:12 -0700821 except SystemExit as e:
jadmanski0afbb632008-06-06 21:10:57 +0000822 exit_code = e.code
Aviv Keshet5c40ec62013-08-20 12:11:12 -0700823 if exit_code:
Aviv Keshet5ae0a002017-05-05 10:23:33 -0700824 logging.exception('Uncaught SystemExit with code %s', exit_code)
825 except Exception:
jadmanski0afbb632008-06-06 21:10:57 +0000826 # If we don't know what happened, we'll classify it as
827 # an 'abort' and return 1.
Aviv Keshet5ae0a002017-05-05 10:23:33 -0700828 logging.exception('Uncaught Exception, exit_code = 1.')
jadmanski0afbb632008-06-06 21:10:57 +0000829 exit_code = 1
830 finally:
mblighff7d61f2008-12-22 14:53:35 +0000831 if pid_file_manager:
832 pid_file_manager.close_file(exit_code)
Fang Deng042c1472014-10-23 13:56:41 -0700833 # Record the autoserv duration time. Must be called
834 # just before the system exits to ensure accuracy.
Paul Hobbs68d98592017-08-22 02:22:49 -0700835 record_autoserv(parser.options, start_time)
jadmanski0afbb632008-06-06 21:10:57 +0000836 sys.exit(exit_code)
mblighfaf0cd42007-11-19 16:00:24 +0000837
mblighbb421852008-03-11 22:36:16 +0000838
mbligha46678d2008-05-01 20:00:01 +0000839if __name__ == '__main__':
jadmanski0afbb632008-06-06 21:10:57 +0000840 main()