blob: 758195ba1bac626acf79a94e8d673546c6356d3d [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
15import signal
Dan Shicf4d2032015-03-12 15:04:21 -070016import socket
Fang Deng042c1472014-10-23 13:56:41 -070017import sys
18import traceback
19import time
20import urllib2
mbligh1ffd5dc2008-11-25 13:24:05 +000021
Prathmesh Prabhua5eecda2016-11-23 16:48:40 -080022
mblighf5427bb2008-04-09 15:55:57 +000023import common
Dan Shia1ecd5c2013-06-06 11:21:31 -070024from autotest_lib.client.common_lib import control_data
Dan Shi32649b82015-08-29 20:53:36 -070025from autotest_lib.client.common_lib import error
Dan Shia1ecd5c2013-06-06 11:21:31 -070026from autotest_lib.client.common_lib import global_config
Dan Shi5ddf6a32015-05-02 00:22:01 -070027from autotest_lib.client.common_lib import utils
Dan Shi37bee222015-04-13 15:46:47 -070028from autotest_lib.client.common_lib.cros.graphite import autotest_es
Prathmesh Prabhua5eecda2016-11-23 16:48:40 -080029
30from chromite.lib import metrics
31
Prashanth Balasubramanianf8b83712014-11-06 15:58:21 -080032try:
33 from autotest_lib.puppylab import results_mocker
34except ImportError:
35 results_mocker = None
36
Dan Shia06f3e22015-09-03 16:15:15 -070037_CONFIG = global_config.global_config
38
39require_atfork = _CONFIG.get_config_value(
mblighcb8cb332009-09-03 21:08:56 +000040 'AUTOSERV', 'require_atfork_module', type=bool, default=True)
41
Dan Shia1ecd5c2013-06-06 11:21:31 -070042
Jakob Jueliche497b552014-09-23 19:11:59 -070043# Number of seconds to wait before returning if testing mode is enabled
Prashanth B6285f6a2014-05-08 18:01:27 -070044TESTING_MODE_SLEEP_SECS = 1
Jakob Jueliche497b552014-09-23 19:11:59 -070045
mblighcb8cb332009-09-03 21:08:56 +000046try:
47 import atfork
48 atfork.monkeypatch_os_fork_functions()
49 import atfork.stdlib_fixer
50 # Fix the Python standard library for threading+fork safety with its
51 # internal locks. http://code.google.com/p/python-atfork/
52 import warnings
53 warnings.filterwarnings('ignore', 'logging module already imported')
54 atfork.stdlib_fixer.fix_logging_module()
55except ImportError, e:
56 from autotest_lib.client.common_lib import global_config
Dan Shia06f3e22015-09-03 16:15:15 -070057 if _CONFIG.get_config_value(
mblighcb8cb332009-09-03 21:08:56 +000058 'AUTOSERV', 'require_atfork_module', type=bool, default=False):
59 print >>sys.stderr, 'Please run utils/build_externals.py'
60 print e
61 sys.exit(1)
mbligh9ff89cd2009-09-03 20:28:17 +000062
Kevin Cheng9b6930f2016-07-20 14:57:15 -070063from autotest_lib.server import frontend
showard75cdfee2009-06-10 17:40:41 +000064from autotest_lib.server import server_logging_config
showard043c62a2009-06-10 19:48:57 +000065from autotest_lib.server import server_job, utils, autoserv_parser, autotest
Dan Shia1ecd5c2013-06-06 11:21:31 -070066from autotest_lib.server import utils as server_utils
Paul Hobbs20cc72a2016-08-30 16:57:05 -070067from autotest_lib.server import site_utils
Kevin Chengadc99f92016-07-20 08:21:58 -070068from autotest_lib.server.cros.dynamic_suite import frontend_wrappers
Dan Shicf4d2032015-03-12 15:04:21 -070069from autotest_lib.site_utils import job_directories
Fang Deng042c1472014-10-23 13:56:41 -070070from autotest_lib.site_utils import job_overhead
Dan Shicf4d2032015-03-12 15:04:21 -070071from autotest_lib.site_utils import lxc
Dan Shi7836d252015-04-27 15:33:58 -070072from autotest_lib.site_utils import lxc_utils
showard75cdfee2009-06-10 17:40:41 +000073from autotest_lib.client.common_lib import pidfile, logging_manager
Gabe Black1e1c41b2015-02-04 23:55:15 -080074from autotest_lib.client.common_lib.cros.graphite import autotest_stats
mbligh92c0fc22008-11-20 16:52:23 +000075
Paul Hobbs20cc72a2016-08-30 16:57:05 -070076
Dan Shicf4d2032015-03-12 15:04:21 -070077# Control segment to stage server-side package.
78STAGE_SERVER_SIDE_PACKAGE_CONTROL_FILE = server_job._control_segment_path(
79 'stage_server_side_package')
80
Dan Shia06f3e22015-09-03 16:15:15 -070081# Command line to start servod in a moblab.
82START_SERVOD_CMD = 'sudo start servod BOARD=%s PORT=%s'
83STOP_SERVOD_CMD = 'sudo stop servod'
84
Alex Millerf1af17e2013-01-09 22:50:32 -080085def log_alarm(signum, frame):
86 logging.error("Received SIGALARM. Ignoring and continuing on.")
Alex Miller0528d6f2013-01-11 10:49:48 -080087 sys.exit(1)
Alex Millerf1af17e2013-01-09 22:50:32 -080088
Dan Shicf4d2032015-03-12 15:04:21 -070089
90def _get_machines(parser):
91 """Get a list of machine names from command line arg -m or a file.
92
93 @param parser: Parser for the command line arguments.
94
95 @return: A list of machine names from command line arg -m or the
96 machines file specified in the command line arg -M.
97 """
98 if parser.options.machines:
99 machines = parser.options.machines.replace(',', ' ').strip().split()
100 else:
101 machines = []
102 machines_file = parser.options.machines_file
103 if machines_file:
104 machines = []
105 for m in open(machines_file, 'r').readlines():
106 # remove comments, spaces
107 m = re.sub('#.*', '', m).strip()
108 if m:
109 machines.append(m)
110 logging.debug('Read list of machines from file: %s', machines_file)
111 logging.debug('Machines: %s', ','.join(machines))
112
113 if machines:
114 for machine in machines:
115 if not machine or re.search('\s', machine):
116 parser.parser.error("Invalid machine: %s" % str(machine))
117 machines = list(set(machines))
118 machines.sort()
119 return machines
120
121
122def _stage_ssp(parser):
123 """Stage server-side package.
124
125 This function calls a control segment to stage server-side package based on
126 the job and autoserv command line option. The detail implementation could
127 be different for each host type. Currently, only CrosHost has
128 stage_server_side_package function defined.
129 The script returns None if no server-side package is available. However,
130 it may raise exception if it failed for reasons other than artifact (the
131 server-side package) not found.
132
133 @param parser: Command line arguments parser passed in the autoserv process.
134
Dan Shi14de7622016-08-22 11:09:06 -0700135 @return: (ssp_url, error_msg), where
136 ssp_url is a url to the autotest server-side package. None if
137 server-side package is not supported.
138 error_msg is a string indicating the failures. None if server-
139 side package is staged successfully.
Dan Shicf4d2032015-03-12 15:04:21 -0700140 """
Kevin Chengadc99f92016-07-20 08:21:58 -0700141 machines_list = _get_machines(parser)
142 if bool(parser.options.lab):
143 machine_dict_list = []
144 afe = frontend_wrappers.RetryingAFE(timeout_min=5, delay_sec=10)
145 for machine in machines_list:
Dan Shi14de7622016-08-22 11:09:06 -0700146 afe_host = afe.get_hosts(hostname=machine)[0]
147 machine_dict_list.append({'hostname': machine,
148 'afe_host': afe_host})
Kevin Chengadc99f92016-07-20 08:21:58 -0700149 machines_list = machine_dict_list
150
Dan Shi36cfd832014-10-10 13:38:51 -0700151 # If test_source_build is not specified, default to use server-side test
152 # code from build specified in --image.
Kevin Chengadc99f92016-07-20 08:21:58 -0700153 namespace = {'machines': machines_list,
Dan Shi36cfd832014-10-10 13:38:51 -0700154 'image': (parser.options.test_source_build or
155 parser.options.image),}
Dan Shicf4d2032015-03-12 15:04:21 -0700156 script_locals = {}
157 execfile(STAGE_SERVER_SIDE_PACKAGE_CONTROL_FILE, namespace, script_locals)
Dan Shi14de7622016-08-22 11:09:06 -0700158 return script_locals['ssp_url'], script_locals['error_msg']
Dan Shicf4d2032015-03-12 15:04:21 -0700159
160
Dan Shiafa63872016-02-23 15:32:31 -0800161def _run_with_ssp(job, container_name, job_id, results, parser, ssp_url,
Dan Shi3be35af2016-08-25 23:22:40 -0700162 job_folder, machines):
Dan Shicf4d2032015-03-12 15:04:21 -0700163 """Run the server job with server-side packaging.
164
Dan Shi37befda2015-12-07 13:16:56 -0800165 @param job: The server job object.
Dan Shicf4d2032015-03-12 15:04:21 -0700166 @param container_name: Name of the container to run the test.
167 @param job_id: ID of the test job.
168 @param results: Folder to store results. This could be different from
169 parser.options.results:
170 parser.options.results can be set to None for results to be
171 stored in a temp folder.
172 results can be None for autoserv run requires no logging.
173 @param parser: Command line parser that contains the options.
174 @param ssp_url: url of the staged server-side package.
Dan Shiafa63872016-02-23 15:32:31 -0800175 @param job_folder: Name of the job result folder.
Dan Shi3be35af2016-08-25 23:22:40 -0700176 @param machines: A list of machines to run the test.
Dan Shicf4d2032015-03-12 15:04:21 -0700177 """
178 bucket = lxc.ContainerBucket()
179 control = (parser.args[0] if len(parser.args) > 0 and parser.args[0] != ''
180 else None)
Dan Shi37befda2015-12-07 13:16:56 -0800181 try:
Dan Shi3be35af2016-08-25 23:22:40 -0700182 dut_name = machines[0] if len(machines) >= 1 else None
Dan Shi37befda2015-12-07 13:16:56 -0800183 test_container = bucket.setup_test(container_name, job_id, ssp_url,
Dan Shiafa63872016-02-23 15:32:31 -0800184 results, control=control,
Dan Shi3be35af2016-08-25 23:22:40 -0700185 job_folder=job_folder,
186 dut_name=dut_name)
Dan Shi37befda2015-12-07 13:16:56 -0800187 except Exception as e:
188 job.record('FAIL', None, None,
189 'Failed to setup container for test: %s. Check logs in '
190 'ssp_logs folder for more details.' % e)
191 raise
192
Dan Shicf4d2032015-03-12 15:04:21 -0700193 args = sys.argv[:]
194 args.remove('--require-ssp')
Dan Shi77b79a62015-07-29 16:22:05 -0700195 # --parent_job_id is only useful in autoserv running in host, not in
196 # container. Include this argument will cause test to fail for builds before
197 # CL 286265 was merged.
198 if '--parent_job_id' in args:
199 index = args.index('--parent_job_id')
200 args.remove('--parent_job_id')
201 # Remove the actual parent job id in command line arg.
202 del args[index]
Dan Shicf4d2032015-03-12 15:04:21 -0700203
204 # A dictionary of paths to replace in the command line. Key is the path to
205 # be replaced with the one in value.
206 paths_to_replace = {}
207 # Replace the control file path with the one in container.
208 if control:
209 container_control_filename = os.path.join(
210 lxc.CONTROL_TEMP_PATH, os.path.basename(control))
211 paths_to_replace[control] = container_control_filename
212 # Update result directory with the one in container.
Dan Shi65374e22016-09-15 16:14:05 -0700213 container_result_dir = os.path.join(lxc.RESULT_DIR_FMT % job_folder)
Dan Shicf4d2032015-03-12 15:04:21 -0700214 if parser.options.results:
Dan Shicf4d2032015-03-12 15:04:21 -0700215 paths_to_replace[parser.options.results] = container_result_dir
216 # Update parse_job directory with the one in container. The assumption is
217 # that the result folder to be parsed is always the same as the results_dir.
218 if parser.options.parse_job:
Dan Shicf4d2032015-03-12 15:04:21 -0700219 paths_to_replace[parser.options.parse_job] = container_result_dir
220
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
Dan Shid1f51232015-04-18 00:29:14 -0700232 cmd_line = ' '.join(["'%s'" % arg if ' ' in arg else arg for arg in args])
Dan Shicf4d2032015-03-12 15:04:21 -0700233 logging.info('Run command in container: %s', cmd_line)
Dan Shi37bee222015-04-13 15:46:47 -0700234 success = False
Dan Shicf4d2032015-03-12 15:04:21 -0700235 try:
236 test_container.attach_run(cmd_line)
Dan Shi37bee222015-04-13 15:46:47 -0700237 success = True
Dan Shi9d3454e2015-12-08 09:16:08 -0800238 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
Dan Shicf4d2032015-03-12 15:04:21 -0700247 finally:
Prathmesh Prabhua5eecda2016-11-23 16:48:40 -0800248 metrics.Counter(
249 'chromeos/autotest/experimental/execute_job_in_ssp').increment(
250 fields={'success': success})
Dan Shi37bee222015-04-13 15:46:47 -0700251 # metadata is uploaded separately so it can use http to upload.
252 metadata = {'drone': socket.gethostname(),
253 'job_id': job_id,
254 'success': success}
255 autotest_es.post(use_http=True,
256 type_str=lxc.CONTAINER_RUN_TEST_METADB_TYPE,
257 metadata=metadata)
Dan Shicf4d2032015-03-12 15:04:21 -0700258 test_container.destroy()
259
260
Dan Shi3f1b8a52015-04-21 11:11:06 -0700261def correct_results_folder_permission(results):
262 """Make sure the results folder has the right permission settings.
263
264 For tests running with server-side packaging, the results folder has the
265 owner of root. This must be changed to the user running the autoserv
266 process, so parsing job can access the results folder.
267 TODO(dshi): crbug.com/459344 Remove this function when test container can be
268 unprivileged container.
269
270 @param results: Path to the results folder.
271
272 """
273 if not results:
274 return
275
Dan Shi32649b82015-08-29 20:53:36 -0700276 try:
277 utils.run('sudo -n chown -R %s "%s"' % (os.getuid(), results))
278 utils.run('sudo -n chgrp -R %s "%s"' % (os.getgid(), results))
279 except error.CmdError as e:
280 metadata = {'error': str(e),
281 'result_folder': results,
282 'drone': socket.gethostname()}
283 autotest_es.post(use_http=True, type_str='correct_results_folder_failure',
284 metadata=metadata)
285 raise
Dan Shi3f1b8a52015-04-21 11:11:06 -0700286
287
Dan Shia06f3e22015-09-03 16:15:15 -0700288def _start_servod(machine):
289 """Try to start servod in moblab if it's not already running or running with
290 different board or port.
291
292 @param machine: Name of the dut used for test.
293 """
294 if not utils.is_moblab():
295 return
296
Dan Shi1cded882015-09-23 16:52:26 -0700297 logging.debug('Trying to start servod.')
Dan Shia06f3e22015-09-03 16:15:15 -0700298 try:
Kevin Cheng9b6930f2016-07-20 14:57:15 -0700299 afe = frontend.AFE()
Dan Shia06f3e22015-09-03 16:15:15 -0700300 board = server_utils.get_board_from_afe(machine, afe)
301 hosts = afe.get_hosts(hostname=machine)
302 servo_host = hosts[0].attributes.get('servo_host', None)
303 servo_port = hosts[0].attributes.get('servo_port', 9999)
304 if not servo_host in ['localhost', '127.0.0.1']:
Dan Shi1cded882015-09-23 16:52:26 -0700305 logging.warn('Starting servod is aborted. The dut\'s servo_host '
306 'attribute is not set to localhost.')
Dan Shia06f3e22015-09-03 16:15:15 -0700307 return
308 except (urllib2.HTTPError, urllib2.URLError):
309 # Ignore error if RPC failed to get board
310 logging.error('Failed to get board name from AFE. Start servod is '
311 'aborted')
312 return
313
314 try:
315 pid = utils.run('pgrep servod').stdout
316 cmd_line = utils.run('ps -fp %s' % pid).stdout
317 if ('--board %s' % board in cmd_line and
318 '--port %s' % servo_port in cmd_line):
319 logging.debug('Servod is already running with given board and port.'
320 ' There is no need to restart servod.')
321 return
322 logging.debug('Servod is running with different board or port. '
323 'Stopping existing servod.')
324 utils.run('sudo stop servod')
325 except error.CmdError:
326 # servod is not running.
327 pass
328
329 try:
330 utils.run(START_SERVOD_CMD % (board, servo_port))
331 logging.debug('Servod is started')
332 except error.CmdError as e:
333 logging.error('Servod failed to be started, error: %s', e)
334
335
Dan Shic68fefb2015-04-07 10:10:52 -0700336def run_autoserv(pid_file_manager, results, parser, ssp_url, use_ssp):
Dan Shicf4d2032015-03-12 15:04:21 -0700337 """Run server job with given options.
338
339 @param pid_file_manager: PidFileManager used to monitor the autoserv process
340 @param results: Folder to store results.
341 @param parser: Parser for the command line arguments.
342 @param ssp_url: Url to server-side package.
Dan Shic68fefb2015-04-07 10:10:52 -0700343 @param use_ssp: Set to True to run with server-side packaging.
Dan Shicf4d2032015-03-12 15:04:21 -0700344 """
Dan Shiec1d47d2015-02-13 11:38:13 -0800345 if parser.options.warn_no_ssp:
Dan Shic68fefb2015-04-07 10:10:52 -0700346 # Post a warning in the log.
Dan Shiec1d47d2015-02-13 11:38:13 -0800347 logging.warn('Autoserv is required to run with server-side packaging. '
348 'However, no drone is found to support server-side '
349 'packaging. The test will be executed in a drone without '
350 'server-side packaging supported.')
351
jadmanski0afbb632008-06-06 21:10:57 +0000352 # send stdin to /dev/null
353 dev_null = os.open(os.devnull, os.O_RDONLY)
354 os.dup2(dev_null, sys.stdin.fileno())
355 os.close(dev_null)
mblighdbf37612007-11-24 19:38:11 +0000356
Dan Shie8aeb662016-06-30 11:22:03 -0700357 # Create separate process group if the process is not a process group
358 # leader. This allows autoserv process to keep running after the caller
359 # process (drone manager call) exits.
360 if os.getpid() != os.getpgid(0):
361 os.setsid()
mbligh1d42d4e2007-11-05 22:42:00 +0000362
Dan Shicf4d2032015-03-12 15:04:21 -0700363 # Container name is predefined so the container can be destroyed in
364 # handle_sigterm.
365 job_or_task_id = job_directories.get_job_id_or_task_id(
366 parser.options.results)
367 container_name = (lxc.TEST_CONTAINER_NAME_FMT %
Dan Shid68d51c2015-04-21 17:00:42 -0700368 (job_or_task_id, time.time(), os.getpid()))
Dan Shiafa63872016-02-23 15:32:31 -0800369 job_folder = job_directories.get_job_folder_name(parser.options.results)
Dan Shicf4d2032015-03-12 15:04:21 -0700370
jadmanski0afbb632008-06-06 21:10:57 +0000371 # Implement SIGTERM handler
mblighc2299562009-07-02 19:00:36 +0000372 def handle_sigterm(signum, frame):
Simran Basi9d9b7292013-10-16 16:44:07 -0700373 logging.debug('Received SIGTERM')
mblighff7d61f2008-12-22 14:53:35 +0000374 if pid_file_manager:
375 pid_file_manager.close_file(1, signal.SIGTERM)
Simran Basi49e21e62013-10-17 12:40:33 -0700376 logging.debug('Finished writing to pid_file. Killing process.')
Dan Shi3f1b8a52015-04-21 11:11:06 -0700377
378 # Update results folder's file permission. This needs to be done ASAP
379 # before the parsing process tries to access the log.
380 if use_ssp and results:
381 correct_results_folder_permission(results)
382
Simran Basid6b83772014-01-06 16:31:30 -0800383 # TODO (sbasi) - remove the time.sleep when crbug.com/302815 is solved.
384 # This sleep allows the pending output to be logged before the kill
385 # signal is sent.
386 time.sleep(.1)
Dan Shic68fefb2015-04-07 10:10:52 -0700387 if use_ssp:
Dan Shicf4d2032015-03-12 15:04:21 -0700388 logging.debug('Destroy container %s before aborting the autoserv '
389 'process.', container_name)
Dan Shi3f1b8a52015-04-21 11:11:06 -0700390 metadata = {'drone': socket.gethostname(),
391 'job_id': job_or_task_id,
392 'container_name': container_name,
393 'action': 'abort',
394 'success': True}
Dan Shicf4d2032015-03-12 15:04:21 -0700395 try:
396 bucket = lxc.ContainerBucket()
397 container = bucket.get(container_name)
398 if container:
399 container.destroy()
400 else:
Dan Shi3f1b8a52015-04-21 11:11:06 -0700401 metadata['success'] = False
402 metadata['error'] = 'container not found'
Dan Shicf4d2032015-03-12 15:04:21 -0700403 logging.debug('Container %s is not found.', container_name)
404 except:
Dan Shi3f1b8a52015-04-21 11:11:06 -0700405 metadata['success'] = False
Dan Shi65374e22016-09-15 16:14:05 -0700406 metadata['error'] = 'Exception: %s' % str(sys.exc_info())
Dan Shicf4d2032015-03-12 15:04:21 -0700407 # Handle any exception so the autoserv process can be aborted.
Dan Shi65374e22016-09-15 16:14:05 -0700408 logging.exception('Failed to destroy container %s.',
409 container_name)
Dan Shi3f1b8a52015-04-21 11:11:06 -0700410 autotest_es.post(use_http=True,
411 type_str=lxc.CONTAINER_RUN_TEST_METADB_TYPE,
412 metadata=metadata)
Dan Shie4a4f9f2015-07-20 09:00:25 -0700413 # Try to correct the result file permission again after the
414 # container is destroyed, as the container might have created some
415 # new files in the result folder.
416 if results:
417 correct_results_folder_permission(results)
Dan Shicf4d2032015-03-12 15:04:21 -0700418
jadmanski0afbb632008-06-06 21:10:57 +0000419 os.killpg(os.getpgrp(), signal.SIGKILL)
mblighfaf0cd42007-11-19 16:00:24 +0000420
jadmanski0afbb632008-06-06 21:10:57 +0000421 # Set signal handler
mblighc2299562009-07-02 19:00:36 +0000422 signal.signal(signal.SIGTERM, handle_sigterm)
mbligha46678d2008-05-01 20:00:01 +0000423
Simran Basid6b83772014-01-06 16:31:30 -0800424 # faulthandler is only needed to debug in the Lab and is not avaliable to
425 # be imported in the chroot as part of VMTest, so Try-Except it.
426 try:
427 import faulthandler
428 faulthandler.register(signal.SIGTERM, all_threads=True, chain=True)
429 logging.debug('faulthandler registered on SIGTERM.')
430 except ImportError:
Christopher Grant4beca022015-06-16 15:14:47 -0400431 sys.exc_clear()
Simran Basid6b83772014-01-06 16:31:30 -0800432
David Rochberg8a60d1e2011-02-01 14:22:07 -0500433 # Ignore SIGTTOU's generated by output from forked children.
434 signal.signal(signal.SIGTTOU, signal.SIG_IGN)
435
Alex Millerf1af17e2013-01-09 22:50:32 -0800436 # If we received a SIGALARM, let's be loud about it.
437 signal.signal(signal.SIGALRM, log_alarm)
438
mbligha5f5e542009-12-30 16:57:49 +0000439 # Server side tests that call shell scripts often depend on $USER being set
440 # but depending on how you launch your autotest scheduler it may not be set.
441 os.environ['USER'] = getpass.getuser()
442
mblighb2bea302008-07-24 20:25:57 +0000443 label = parser.options.label
mbligh374f3412009-05-13 21:29:45 +0000444 group_name = parser.options.group_name
mblighb2bea302008-07-24 20:25:57 +0000445 user = parser.options.user
446 client = parser.options.client
447 server = parser.options.server
jadmanski0afbb632008-06-06 21:10:57 +0000448 install_before = parser.options.install_before
mblighb2bea302008-07-24 20:25:57 +0000449 install_after = parser.options.install_after
450 verify = parser.options.verify
451 repair = parser.options.repair
showard45ae8192008-11-05 19:32:53 +0000452 cleanup = parser.options.cleanup
Alex Millercb79ba72013-05-29 14:43:00 -0700453 provision = parser.options.provision
Dan Shi07e09af2013-04-12 09:31:29 -0700454 reset = parser.options.reset
Alex Miller667b5f22014-02-28 15:33:39 -0800455 job_labels = parser.options.job_labels
mblighb2bea302008-07-24 20:25:57 +0000456 no_tee = parser.options.no_tee
jadmanski0afbb632008-06-06 21:10:57 +0000457 parse_job = parser.options.parse_job
mblighe7d9c602009-07-02 19:02:33 +0000458 execution_tag = parser.options.execution_tag
459 if not execution_tag:
460 execution_tag = parse_job
jadmanski0afbb632008-06-06 21:10:57 +0000461 ssh_user = parser.options.ssh_user
462 ssh_port = parser.options.ssh_port
463 ssh_pass = parser.options.ssh_pass
jadmanskidef0c3c2009-03-25 20:07:10 +0000464 collect_crashinfo = parser.options.collect_crashinfo
mblighe0cbc912010-03-11 18:03:07 +0000465 control_filename = parser.options.control_filename
Scott Zawalski91493c82013-01-25 16:15:20 -0500466 test_retry = parser.options.test_retry
beepscb6f1e22013-06-28 19:14:10 -0700467 verify_job_repo_url = parser.options.verify_job_repo_url
Christopher Wileyf594c5e2013-07-03 18:25:30 -0700468 skip_crash_collection = parser.options.skip_crash_collection
Aviv Keshet18ee3142013-08-12 15:01:51 -0700469 ssh_verbosity = int(parser.options.ssh_verbosity)
Fang Deng6cc20de2013-09-06 15:47:32 -0700470 ssh_options = parser.options.ssh_options
Dan Shib669cbd2013-09-13 11:17:17 -0700471 no_use_packaging = parser.options.no_use_packaging
Simran Basi1bf60eb2015-12-01 16:39:29 -0800472 host_attributes = parser.options.host_attributes
473 in_lab = bool(parser.options.lab)
mbligha46678d2008-05-01 20:00:01 +0000474
mblighb2bea302008-07-24 20:25:57 +0000475 # can't be both a client and a server side test
476 if client and server:
Eric Li861b2d52011-02-04 14:50:35 -0800477 parser.parser.error("Can not specify a test as both server and client!")
mblighb2bea302008-07-24 20:25:57 +0000478
Alex Millercb79ba72013-05-29 14:43:00 -0700479 if provision and client:
480 parser.parser.error("Cannot specify provisioning and client!")
481
482 is_special_task = (verify or repair or cleanup or collect_crashinfo or
Dan Shi07e09af2013-04-12 09:31:29 -0700483 provision or reset)
Alex Millercb79ba72013-05-29 14:43:00 -0700484 if len(parser.args) < 1 and not is_special_task:
Eric Li861b2d52011-02-04 14:50:35 -0800485 parser.parser.error("Missing argument: control file")
mbligha46678d2008-05-01 20:00:01 +0000486
Aviv Keshet18ee3142013-08-12 15:01:51 -0700487 if ssh_verbosity > 0:
488 # ssh_verbosity is an integer between 0 and 3, inclusive
489 ssh_verbosity_flag = '-' + 'v' * ssh_verbosity
Fang Dengd1c2b732013-08-20 12:59:46 -0700490 else:
491 ssh_verbosity_flag = ''
Aviv Keshet18ee3142013-08-12 15:01:51 -0700492
showard45ae8192008-11-05 19:32:53 +0000493 # We have a control file unless it's just a verify/repair/cleanup job
jadmanski0afbb632008-06-06 21:10:57 +0000494 if len(parser.args) > 0:
495 control = parser.args[0]
496 else:
497 control = None
mbligha46678d2008-05-01 20:00:01 +0000498
Dan Shicf4d2032015-03-12 15:04:21 -0700499 machines = _get_machines(parser)
mbligh374f3412009-05-13 21:29:45 +0000500 if group_name and len(machines) < 2:
Dan Shicf4d2032015-03-12 15:04:21 -0700501 parser.parser.error('-G %r may only be supplied with more than one '
502 'machine.' % group_name)
mbligh374f3412009-05-13 21:29:45 +0000503
Christopher Wiley8a91f232013-07-09 11:02:27 -0700504 kwargs = {'group_name': group_name, 'tag': execution_tag,
Dan Shicf4d2032015-03-12 15:04:21 -0700505 'disable_sysinfo': parser.options.disable_sysinfo}
Dan Shi70647ca2015-07-16 22:52:35 -0700506 if parser.options.parent_job_id:
507 kwargs['parent_job_id'] = int(parser.options.parent_job_id)
mblighe0cbc912010-03-11 18:03:07 +0000508 if control_filename:
509 kwargs['control_filename'] = control_filename
Simran Basi1bf60eb2015-12-01 16:39:29 -0800510 if host_attributes:
511 kwargs['host_attributes'] = host_attributes
512 kwargs['in_lab'] = in_lab
jadmanski0afbb632008-06-06 21:10:57 +0000513 job = server_job.server_job(control, parser.args[1:], results, label,
514 user, machines, client, parse_job,
Fang Dengd1c2b732013-08-20 12:59:46 -0700515 ssh_user, ssh_port, ssh_pass,
Aviv Keshetc5947fa2013-09-04 14:06:29 -0700516 ssh_verbosity_flag, ssh_options,
517 test_retry, **kwargs)
Dan Shicf4d2032015-03-12 15:04:21 -0700518
showard75cdfee2009-06-10 17:40:41 +0000519 job.logging.start_logging()
mbligh4608b002010-01-05 18:22:35 +0000520 job.init_parser()
mbligha46678d2008-05-01 20:00:01 +0000521
mbligh161fe6f2008-06-19 16:26:04 +0000522 # perform checks
523 job.precheck()
524
jadmanski0afbb632008-06-06 21:10:57 +0000525 # run the job
526 exit_code = 0
Dan Shic1b8bdd2015-09-14 23:11:24 -0700527 auto_start_servod = _CONFIG.get_config_value(
528 'AUTOSERV', 'auto_start_servod', type=bool, default=False)
Paul Hobbs20cc72a2016-08-30 16:57:05 -0700529
jadmanski0afbb632008-06-06 21:10:57 +0000530 try:
Paul Hobbs20cc72a2016-08-30 16:57:05 -0700531 with site_utils.SetupTsMonGlobalState('autoserv', indirect=True, short_lived=True):
532 try:
533 if repair:
534 if auto_start_servod and len(machines) == 1:
535 _start_servod(machines[0])
536 job.repair(job_labels)
537 elif verify:
538 job.verify(job_labels)
539 elif provision:
540 job.provision(job_labels)
541 elif reset:
542 job.reset(job_labels)
543 elif cleanup:
544 job.cleanup(job_labels)
Dan Shicf4d2032015-03-12 15:04:21 -0700545 else:
Paul Hobbs20cc72a2016-08-30 16:57:05 -0700546 if auto_start_servod and len(machines) == 1:
547 _start_servod(machines[0])
548 if use_ssp:
Dan Shiafa63872016-02-23 15:32:31 -0800549 try:
Paul Hobbs20cc72a2016-08-30 16:57:05 -0700550 _run_with_ssp(job, container_name, job_or_task_id,
551 results, parser, ssp_url, job_folder,
552 machines)
553 finally:
554 # Update the ownership of files in result folder.
Dan Shiafa63872016-02-23 15:32:31 -0800555 correct_results_folder_permission(results)
Paul Hobbs20cc72a2016-08-30 16:57:05 -0700556 else:
557 if collect_crashinfo:
558 # Update the ownership of files in result folder. If the
559 # job to collect crashinfo was running inside container
560 # (SSP) and crashed before correcting folder permission,
561 # the result folder might have wrong permission setting.
562 try:
563 correct_results_folder_permission(results)
564 except:
565 # Ignore any error as the user may not have root
566 # permission to run sudo command.
567 pass
568 job.run(install_before, install_after,
569 verify_job_repo_url=verify_job_repo_url,
570 only_collect_crashinfo=collect_crashinfo,
571 skip_crash_collection=skip_crash_collection,
572 job_labels=job_labels,
573 use_packaging=(not no_use_packaging))
574 finally:
575 while job.hosts:
576 host = job.hosts.pop()
577 host.close()
jadmanski0afbb632008-06-06 21:10:57 +0000578 except:
jadmanski27b37ea2008-10-29 23:54:31 +0000579 exit_code = 1
jadmanski0afbb632008-06-06 21:10:57 +0000580 traceback.print_exc()
mbligha46678d2008-05-01 20:00:01 +0000581
mblighff7d61f2008-12-22 14:53:35 +0000582 if pid_file_manager:
583 pid_file_manager.num_tests_failed = job.num_tests_failed
584 pid_file_manager.close_file(exit_code)
jadmanskie0dffc32008-12-15 17:30:30 +0000585 job.cleanup_parser()
showard21baa452008-10-21 00:08:39 +0000586
jadmanski27b37ea2008-10-29 23:54:31 +0000587 sys.exit(exit_code)
mbligha46678d2008-05-01 20:00:01 +0000588
589
Fang Deng042c1472014-10-23 13:56:41 -0700590def record_autoserv(options, duration_secs):
591 """Record autoserv end-to-end time in metadata db.
592
593 @param options: parser options.
594 @param duration_secs: How long autoserv has taken, in secs.
595 """
596 # Get machine hostname
597 machines = options.machines.replace(
598 ',', ' ').strip().split() if options.machines else []
599 num_machines = len(machines)
600 if num_machines > 1:
601 # Skip the case where atomic group is used.
602 return
603 elif num_machines == 0:
604 machines.append('hostless')
605
606 # Determine the status that will be reported.
607 s = job_overhead.STATUS
608 task_mapping = {
609 'reset': s.RESETTING, 'verify': s.VERIFYING,
610 'provision': s.PROVISIONING, 'repair': s.REPAIRING,
611 'cleanup': s.CLEANING, 'collect_crashinfo': s.GATHERING}
Dan Shi888cfca2015-07-31 15:49:00 -0700612 match = filter(lambda task: getattr(options, task, False) == True,
613 task_mapping)
Fang Deng042c1472014-10-23 13:56:41 -0700614 status = task_mapping[match[0]] if match else s.RUNNING
615 is_special_task = status not in [s.RUNNING, s.GATHERING]
Dan Shicf4d2032015-03-12 15:04:21 -0700616 job_or_task_id = job_directories.get_job_id_or_task_id(options.results)
Fang Deng042c1472014-10-23 13:56:41 -0700617 job_overhead.record_state_duration(
618 job_or_task_id, machines[0], status, duration_secs,
619 is_special_task=is_special_task)
620
621
mbligha46678d2008-05-01 20:00:01 +0000622def main():
Fang Deng042c1472014-10-23 13:56:41 -0700623 start_time = datetime.datetime.now()
Dan Shia1ecd5c2013-06-06 11:21:31 -0700624 # White list of tests with run time measurement enabled.
Dan Shia06f3e22015-09-03 16:15:15 -0700625 measure_run_time_tests_names = _CONFIG.get_config_value(
626 'AUTOSERV', 'measure_run_time_tests', type=str)
Dan Shia1ecd5c2013-06-06 11:21:31 -0700627 if measure_run_time_tests_names:
628 measure_run_time_tests = [t.strip() for t in
629 measure_run_time_tests_names.split(',')]
630 else:
631 measure_run_time_tests = []
jadmanski0afbb632008-06-06 21:10:57 +0000632 # grab the parser
633 parser = autoserv_parser.autoserv_parser
mbligha5cb4062009-02-17 15:53:39 +0000634 parser.parse_args()
mbligha46678d2008-05-01 20:00:01 +0000635
jadmanski0afbb632008-06-06 21:10:57 +0000636 if len(sys.argv) == 1:
637 parser.parser.print_help()
638 sys.exit(1)
mbligha6f13082008-06-05 23:53:46 +0000639
Dan Shicf4d2032015-03-12 15:04:21 -0700640 # If the job requires to run with server-side package, try to stage server-
641 # side package first. If that fails with error that autotest server package
Dan Shic68fefb2015-04-07 10:10:52 -0700642 # does not exist, fall back to run the job without using server-side
643 # packaging. If option warn_no_ssp is specified, that means autoserv is
644 # running in a drone does not support SSP, thus no need to stage server-side
645 # package.
Dan Shicf4d2032015-03-12 15:04:21 -0700646 ssp_url = None
Dan Shi0b754c52015-04-20 14:20:38 -0700647 ssp_url_warning = False
Dan Shic68fefb2015-04-07 10:10:52 -0700648 if (not parser.options.warn_no_ssp and parser.options.require_ssp):
Dan Shi14de7622016-08-22 11:09:06 -0700649 ssp_url, ssp_error_msg = _stage_ssp(parser)
Dan Shi0b754c52015-04-20 14:20:38 -0700650 # The build does not have autotest server package. Fall back to not
651 # to use server-side package. Logging is postponed until logging being
652 # set up.
653 ssp_url_warning = not ssp_url
Dan Shicf4d2032015-03-12 15:04:21 -0700654
showard75cdfee2009-06-10 17:40:41 +0000655 if parser.options.no_logging:
656 results = None
657 else:
658 results = parser.options.results
mbligh80e1eba2008-11-19 00:26:18 +0000659 if not results:
660 results = 'results.' + time.strftime('%Y-%m-%d-%H.%M.%S')
Dan Shi14de7622016-08-22 11:09:06 -0700661 results = os.path.abspath(results)
showard566d3c02010-01-12 18:57:01 +0000662 resultdir_exists = False
663 for filename in ('control.srv', 'status.log', '.autoserv_execute'):
664 if os.path.exists(os.path.join(results, filename)):
665 resultdir_exists = True
mbligh4608b002010-01-05 18:22:35 +0000666 if not parser.options.use_existing_results and resultdir_exists:
mbligh80e1eba2008-11-19 00:26:18 +0000667 error = "Error: results directory already exists: %s\n" % results
668 sys.stderr.write(error)
669 sys.exit(1)
mbligha788dc42009-03-26 21:10:16 +0000670
671 # Now that we certified that there's no leftover results dir from
672 # previous jobs, lets create the result dir since the logging system
673 # needs to create the log file in there.
674 if not os.path.isdir(results):
675 os.makedirs(results)
showard75cdfee2009-06-10 17:40:41 +0000676
Dan Shic68fefb2015-04-07 10:10:52 -0700677 # Server-side packaging will only be used if it's required and the package
678 # is available. If warn_no_ssp is specified, it means that autoserv is
679 # running in a drone does not have SSP supported and a warning will be logs.
680 # Therefore, it should not run with SSP.
681 use_ssp = (not parser.options.warn_no_ssp and parser.options.require_ssp
682 and ssp_url)
683 if use_ssp:
Dan Shie28de552015-05-06 16:51:58 -0700684 log_dir = os.path.join(results, 'ssp_logs') if results else None
Dan Shicf4d2032015-03-12 15:04:21 -0700685 if log_dir and not os.path.exists(log_dir):
686 os.makedirs(log_dir)
687 else:
688 log_dir = results
Dan Shi3f1b8a52015-04-21 11:11:06 -0700689
showard75cdfee2009-06-10 17:40:41 +0000690 logging_manager.configure_logging(
Dan Shicf4d2032015-03-12 15:04:21 -0700691 server_logging_config.ServerLoggingConfig(),
692 results_dir=log_dir,
showard10d84172009-06-18 23:16:50 +0000693 use_console=not parser.options.no_tee,
694 verbose=parser.options.verbose,
695 no_console_prefix=parser.options.no_console_prefix)
Dan Shicf4d2032015-03-12 15:04:21 -0700696
Dan Shi0b754c52015-04-20 14:20:38 -0700697 if ssp_url_warning:
698 logging.warn(
699 'Autoserv is required to run with server-side packaging. '
700 'However, no server-side package can be found based on '
Dan Shi6450e142016-03-11 11:52:20 -0800701 '`--image`, host attribute job_repo_url or host OS version '
702 'label. It could be that the build to test is older than the '
703 'minimum version that supports server-side packaging. The test '
Dan Shi14de7622016-08-22 11:09:06 -0700704 'will be executed without using erver-side packaging. '
705 'Following is the detailed error:\n%s', ssp_error_msg)
Dan Shi0b754c52015-04-20 14:20:38 -0700706
showard75cdfee2009-06-10 17:40:41 +0000707 if results:
mbligha788dc42009-03-26 21:10:16 +0000708 logging.info("Results placed in %s" % results)
mbligh10717632008-11-19 00:21:57 +0000709
mbligh4608b002010-01-05 18:22:35 +0000710 # wait until now to perform this check, so it get properly logged
Dan Shicf4d2032015-03-12 15:04:21 -0700711 if (parser.options.use_existing_results and not resultdir_exists and
Dan Shiff78f112015-06-12 13:34:02 -0700712 not utils.is_in_container()):
mbligh4608b002010-01-05 18:22:35 +0000713 logging.error("No existing results directory found: %s", results)
714 sys.exit(1)
715
Dan Shicf4d2032015-03-12 15:04:21 -0700716 logging.debug('autoserv is running in drone %s.', socket.gethostname())
Aviv Keshet5c40ec62013-08-20 12:11:12 -0700717 logging.debug('autoserv command was: %s', ' '.join(sys.argv))
mbligh4608b002010-01-05 18:22:35 +0000718
Dan Shicf4d2032015-03-12 15:04:21 -0700719 if parser.options.write_pidfile and results:
mbligh4608b002010-01-05 18:22:35 +0000720 pid_file_manager = pidfile.PidFileManager(parser.options.pidfile_label,
721 results)
jadmanskid5ab8c52008-12-03 16:27:07 +0000722 pid_file_manager.open_file()
mblighff7d61f2008-12-22 14:53:35 +0000723 else:
724 pid_file_manager = None
mbligha46678d2008-05-01 20:00:01 +0000725
jadmanskif22fea82008-11-26 20:57:07 +0000726 autotest.BaseAutotest.set_install_in_tmpdir(
727 parser.options.install_in_tmpdir)
728
Dan Shia1ecd5c2013-06-06 11:21:31 -0700729 timer = None
730 try:
731 # Take the first argument as control file name, get the test name from
732 # the control file. If the test name exists in the list of tests with
733 # run time measurement enabled, start a timer to begin measurement.
734 if (len(parser.args) > 0 and parser.args[0] != '' and
735 parser.options.machines):
Dan Shibbc16132013-07-09 16:23:59 -0700736 try:
737 test_name = control_data.parse_control(parser.args[0],
738 raise_warnings=True).name
739 except control_data.ControlVariableException:
740 logging.debug('Failed to retrieve test name from control file.')
741 test_name = None
Dan Shia1ecd5c2013-06-06 11:21:31 -0700742 if test_name in measure_run_time_tests:
743 machines = parser.options.machines.replace(',', ' '
744 ).strip().split()
Dan Shi8eac5af2014-09-17 00:15:15 -0700745 try:
Kevin Cheng9b6930f2016-07-20 14:57:15 -0700746 afe = frontend.AFE()
Dan Shi8eac5af2014-09-17 00:15:15 -0700747 board = server_utils.get_board_from_afe(machines[0], afe)
Gabe Black1e1c41b2015-02-04 23:55:15 -0800748 timer = autotest_stats.Timer('autoserv_run_time.%s.%s' %
749 (board, test_name))
Dan Shi8eac5af2014-09-17 00:15:15 -0700750 timer.start()
751 except (urllib2.HTTPError, urllib2.URLError):
752 # Ignore error if RPC failed to get board
753 pass
Dan Shia1ecd5c2013-06-06 11:21:31 -0700754 except control_data.ControlVariableException as e:
755 logging.error(str(e))
jadmanski0afbb632008-06-06 21:10:57 +0000756 exit_code = 0
Prashanth B6285f6a2014-05-08 18:01:27 -0700757 # TODO(beeps): Extend this to cover different failure modes.
758 # Testing exceptions are matched against labels sent to autoserv. Eg,
759 # to allow only the hostless job to run, specify
760 # testing_exceptions: test_suite in the shadow_config. To allow both
761 # the hostless job and dummy_Pass to run, specify
762 # testing_exceptions: test_suite,dummy_Pass. You can figure out
763 # what label autoserv is invoked with by looking through the logs of a test
764 # for the autoserv command's -l option.
Dan Shia06f3e22015-09-03 16:15:15 -0700765 testing_exceptions = _CONFIG.get_config_value(
Prashanth B6285f6a2014-05-08 18:01:27 -0700766 'AUTOSERV', 'testing_exceptions', type=list, default=[])
Dan Shia06f3e22015-09-03 16:15:15 -0700767 test_mode = _CONFIG.get_config_value(
Prashanth B6285f6a2014-05-08 18:01:27 -0700768 'AUTOSERV', 'testing_mode', type=bool, default=False)
Prashanth Balasubramanianf8b83712014-11-06 15:58:21 -0800769 test_mode = (results_mocker and test_mode and not
770 any([ex in parser.options.label
771 for ex in testing_exceptions]))
772 is_task = (parser.options.verify or parser.options.repair or
773 parser.options.provision or parser.options.reset or
774 parser.options.cleanup or parser.options.collect_crashinfo)
jadmanski0afbb632008-06-06 21:10:57 +0000775 try:
776 try:
Prashanth B6285f6a2014-05-08 18:01:27 -0700777 if test_mode:
Prashanth Balasubramanianf8b83712014-11-06 15:58:21 -0800778 # The parser doesn't run on tasks anyway, so we can just return
779 # happy signals without faking results.
780 if not is_task:
781 machine = parser.options.results.split('/')[-1]
782
783 # TODO(beeps): The proper way to do this would be to
784 # refactor job creation so we can invoke job.record
785 # directly. To do that one needs to pipe the test_name
786 # through run_autoserv and bail just before invoking
787 # the server job. See the comment in
788 # puppylab/results_mocker for more context.
789 results_mocker.ResultsMocker(
Prashanth Balasubramanian22dd2262014-11-28 18:19:18 -0800790 test_name if test_name else 'unknown-test',
791 parser.options.results, machine
Prashanth Balasubramanianf8b83712014-11-06 15:58:21 -0800792 ).mock_results()
793 return
Prashanth B6285f6a2014-05-08 18:01:27 -0700794 else:
Dan Shic68fefb2015-04-07 10:10:52 -0700795 run_autoserv(pid_file_manager, results, parser, ssp_url,
796 use_ssp)
Aviv Keshet5c40ec62013-08-20 12:11:12 -0700797 except SystemExit as e:
jadmanski0afbb632008-06-06 21:10:57 +0000798 exit_code = e.code
Aviv Keshet5c40ec62013-08-20 12:11:12 -0700799 if exit_code:
800 logging.exception(e)
801 except Exception as e:
jadmanski0afbb632008-06-06 21:10:57 +0000802 # If we don't know what happened, we'll classify it as
803 # an 'abort' and return 1.
Aviv Keshet5c40ec62013-08-20 12:11:12 -0700804 logging.exception(e)
jadmanski0afbb632008-06-06 21:10:57 +0000805 exit_code = 1
806 finally:
mblighff7d61f2008-12-22 14:53:35 +0000807 if pid_file_manager:
808 pid_file_manager.close_file(exit_code)
Dan Shia1ecd5c2013-06-06 11:21:31 -0700809 if timer:
810 timer.stop()
Fang Deng042c1472014-10-23 13:56:41 -0700811 # Record the autoserv duration time. Must be called
812 # just before the system exits to ensure accuracy.
813 duration_secs = (datetime.datetime.now() - start_time).total_seconds()
814 record_autoserv(parser.options, duration_secs)
jadmanski0afbb632008-06-06 21:10:57 +0000815 sys.exit(exit_code)
mblighfaf0cd42007-11-19 16:00:24 +0000816
mblighbb421852008-03-11 22:36:16 +0000817
mbligha46678d2008-05-01 20:00:01 +0000818if __name__ == '__main__':
jadmanski0afbb632008-06-06 21:10:57 +0000819 main()