blob: d5e6ce30409288a5e0248530c9e3454162c0b6a3 [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 ast
10import datetime
11import 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
mblighf5427bb2008-04-09 15:55:57 +000022import common
mbligh9ff89cd2009-09-03 20:28:17 +000023
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
29from autotest_lib.client.common_lib.cros.graphite import autotest_stats
Prashanth Balasubramanianf8b83712014-11-06 15:58:21 -080030try:
31 from autotest_lib.puppylab import results_mocker
32except ImportError:
33 results_mocker = None
34
Dan Shia06f3e22015-09-03 16:15:15 -070035_CONFIG = global_config.global_config
36
37require_atfork = _CONFIG.get_config_value(
mblighcb8cb332009-09-03 21:08:56 +000038 'AUTOSERV', 'require_atfork_module', type=bool, default=True)
39
Dan Shia1ecd5c2013-06-06 11:21:31 -070040
Jakob Jueliche497b552014-09-23 19:11:59 -070041# Number of seconds to wait before returning if testing mode is enabled
Prashanth B6285f6a2014-05-08 18:01:27 -070042TESTING_MODE_SLEEP_SECS = 1
Jakob Jueliche497b552014-09-23 19:11:59 -070043
mblighcb8cb332009-09-03 21:08:56 +000044try:
45 import atfork
46 atfork.monkeypatch_os_fork_functions()
47 import atfork.stdlib_fixer
48 # Fix the Python standard library for threading+fork safety with its
49 # internal locks. http://code.google.com/p/python-atfork/
50 import warnings
51 warnings.filterwarnings('ignore', 'logging module already imported')
52 atfork.stdlib_fixer.fix_logging_module()
53except ImportError, e:
54 from autotest_lib.client.common_lib import global_config
Dan Shia06f3e22015-09-03 16:15:15 -070055 if _CONFIG.get_config_value(
mblighcb8cb332009-09-03 21:08:56 +000056 'AUTOSERV', 'require_atfork_module', type=bool, default=False):
57 print >>sys.stderr, 'Please run utils/build_externals.py'
58 print e
59 sys.exit(1)
mbligh9ff89cd2009-09-03 20:28:17 +000060
Kevin Cheng9b6930f2016-07-20 14:57:15 -070061from autotest_lib.server import frontend
showard75cdfee2009-06-10 17:40:41 +000062from autotest_lib.server import server_logging_config
showard043c62a2009-06-10 19:48:57 +000063from autotest_lib.server import server_job, utils, autoserv_parser, autotest
Dan Shia1ecd5c2013-06-06 11:21:31 -070064from autotest_lib.server import utils as server_utils
Kevin Chengadc99f92016-07-20 08:21:58 -070065from autotest_lib.server.cros.dynamic_suite import frontend_wrappers
Dan Shicf4d2032015-03-12 15:04:21 -070066from autotest_lib.site_utils import job_directories
Fang Deng042c1472014-10-23 13:56:41 -070067from autotest_lib.site_utils import job_overhead
Dan Shicf4d2032015-03-12 15:04:21 -070068from autotest_lib.site_utils import lxc
Dan Shi7836d252015-04-27 15:33:58 -070069from autotest_lib.site_utils import lxc_utils
showard75cdfee2009-06-10 17:40:41 +000070from autotest_lib.client.common_lib import pidfile, logging_manager
Gabe Black1e1c41b2015-02-04 23:55:15 -080071from autotest_lib.client.common_lib.cros.graphite import autotest_stats
mbligh92c0fc22008-11-20 16:52:23 +000072
Dan Shicf4d2032015-03-12 15:04:21 -070073# Control segment to stage server-side package.
74STAGE_SERVER_SIDE_PACKAGE_CONTROL_FILE = server_job._control_segment_path(
75 'stage_server_side_package')
76
Dan Shia06f3e22015-09-03 16:15:15 -070077# Command line to start servod in a moblab.
78START_SERVOD_CMD = 'sudo start servod BOARD=%s PORT=%s'
79STOP_SERVOD_CMD = 'sudo stop servod'
80
Alex Millerf1af17e2013-01-09 22:50:32 -080081def log_alarm(signum, frame):
82 logging.error("Received SIGALARM. Ignoring and continuing on.")
Alex Miller0528d6f2013-01-11 10:49:48 -080083 sys.exit(1)
Alex Millerf1af17e2013-01-09 22:50:32 -080084
Dan Shicf4d2032015-03-12 15:04:21 -070085
86def _get_machines(parser):
87 """Get a list of machine names from command line arg -m or a file.
88
89 @param parser: Parser for the command line arguments.
90
91 @return: A list of machine names from command line arg -m or the
92 machines file specified in the command line arg -M.
93 """
94 if parser.options.machines:
95 machines = parser.options.machines.replace(',', ' ').strip().split()
96 else:
97 machines = []
98 machines_file = parser.options.machines_file
99 if machines_file:
100 machines = []
101 for m in open(machines_file, 'r').readlines():
102 # remove comments, spaces
103 m = re.sub('#.*', '', m).strip()
104 if m:
105 machines.append(m)
106 logging.debug('Read list of machines from file: %s', machines_file)
107 logging.debug('Machines: %s', ','.join(machines))
108
109 if machines:
110 for machine in machines:
111 if not machine or re.search('\s', machine):
112 parser.parser.error("Invalid machine: %s" % str(machine))
113 machines = list(set(machines))
114 machines.sort()
115 return machines
116
117
118def _stage_ssp(parser):
119 """Stage server-side package.
120
121 This function calls a control segment to stage server-side package based on
122 the job and autoserv command line option. The detail implementation could
123 be different for each host type. Currently, only CrosHost has
124 stage_server_side_package function defined.
125 The script returns None if no server-side package is available. However,
126 it may raise exception if it failed for reasons other than artifact (the
127 server-side package) not found.
128
129 @param parser: Command line arguments parser passed in the autoserv process.
130
Dan Shi14de7622016-08-22 11:09:06 -0700131 @return: (ssp_url, error_msg), where
132 ssp_url is a url to the autotest server-side package. None if
133 server-side package is not supported.
134 error_msg is a string indicating the failures. None if server-
135 side package is staged successfully.
Dan Shicf4d2032015-03-12 15:04:21 -0700136 """
Kevin Chengadc99f92016-07-20 08:21:58 -0700137 machines_list = _get_machines(parser)
138 if bool(parser.options.lab):
139 machine_dict_list = []
140 afe = frontend_wrappers.RetryingAFE(timeout_min=5, delay_sec=10)
141 for machine in machines_list:
Dan Shi14de7622016-08-22 11:09:06 -0700142 afe_host = afe.get_hosts(hostname=machine)[0]
143 machine_dict_list.append({'hostname': machine,
144 'afe_host': afe_host})
Kevin Chengadc99f92016-07-20 08:21:58 -0700145 machines_list = machine_dict_list
146
Dan Shi36cfd832014-10-10 13:38:51 -0700147 # If test_source_build is not specified, default to use server-side test
148 # code from build specified in --image.
Kevin Chengadc99f92016-07-20 08:21:58 -0700149 namespace = {'machines': machines_list,
Dan Shi36cfd832014-10-10 13:38:51 -0700150 'image': (parser.options.test_source_build or
151 parser.options.image),}
Dan Shicf4d2032015-03-12 15:04:21 -0700152 script_locals = {}
153 execfile(STAGE_SERVER_SIDE_PACKAGE_CONTROL_FILE, namespace, script_locals)
Dan Shi14de7622016-08-22 11:09:06 -0700154 return script_locals['ssp_url'], script_locals['error_msg']
Dan Shicf4d2032015-03-12 15:04:21 -0700155
156
Dan Shiafa63872016-02-23 15:32:31 -0800157def _run_with_ssp(job, container_name, job_id, results, parser, ssp_url,
158 job_folder):
Dan Shicf4d2032015-03-12 15:04:21 -0700159 """Run the server job with server-side packaging.
160
Dan Shi37befda2015-12-07 13:16:56 -0800161 @param job: The server job object.
Dan Shicf4d2032015-03-12 15:04:21 -0700162 @param container_name: Name of the container to run the test.
163 @param job_id: ID of the test job.
164 @param results: Folder to store results. This could be different from
165 parser.options.results:
166 parser.options.results can be set to None for results to be
167 stored in a temp folder.
168 results can be None for autoserv run requires no logging.
169 @param parser: Command line parser that contains the options.
170 @param ssp_url: url of the staged server-side package.
Dan Shiafa63872016-02-23 15:32:31 -0800171 @param job_folder: Name of the job result folder.
Dan Shicf4d2032015-03-12 15:04:21 -0700172 """
173 bucket = lxc.ContainerBucket()
174 control = (parser.args[0] if len(parser.args) > 0 and parser.args[0] != ''
175 else None)
Dan Shi37befda2015-12-07 13:16:56 -0800176 try:
177 test_container = bucket.setup_test(container_name, job_id, ssp_url,
Dan Shiafa63872016-02-23 15:32:31 -0800178 results, control=control,
179 job_folder=job_folder)
Dan Shi37befda2015-12-07 13:16:56 -0800180 except Exception as e:
181 job.record('FAIL', None, None,
182 'Failed to setup container for test: %s. Check logs in '
183 'ssp_logs folder for more details.' % e)
184 raise
185
Dan Shicf4d2032015-03-12 15:04:21 -0700186 args = sys.argv[:]
187 args.remove('--require-ssp')
Dan Shi77b79a62015-07-29 16:22:05 -0700188 # --parent_job_id is only useful in autoserv running in host, not in
189 # container. Include this argument will cause test to fail for builds before
190 # CL 286265 was merged.
191 if '--parent_job_id' in args:
192 index = args.index('--parent_job_id')
193 args.remove('--parent_job_id')
194 # Remove the actual parent job id in command line arg.
195 del args[index]
Dan Shicf4d2032015-03-12 15:04:21 -0700196
197 # A dictionary of paths to replace in the command line. Key is the path to
198 # be replaced with the one in value.
199 paths_to_replace = {}
200 # Replace the control file path with the one in container.
201 if control:
202 container_control_filename = os.path.join(
203 lxc.CONTROL_TEMP_PATH, os.path.basename(control))
204 paths_to_replace[control] = container_control_filename
205 # Update result directory with the one in container.
206 if parser.options.results:
Dan Shiafa63872016-02-23 15:32:31 -0800207 container_result_dir = os.path.join(lxc.RESULT_DIR_FMT % job_folder)
Dan Shicf4d2032015-03-12 15:04:21 -0700208 paths_to_replace[parser.options.results] = container_result_dir
209 # Update parse_job directory with the one in container. The assumption is
210 # that the result folder to be parsed is always the same as the results_dir.
211 if parser.options.parse_job:
Dan Shiafa63872016-02-23 15:32:31 -0800212 container_parse_dir = os.path.join(lxc.RESULT_DIR_FMT % job_folder)
Dan Shicf4d2032015-03-12 15:04:21 -0700213 paths_to_replace[parser.options.parse_job] = container_result_dir
214
215 args = [paths_to_replace.get(arg, arg) for arg in args]
216
217 # Apply --use-existing-results, results directory is aready created and
218 # mounted in container. Apply this arg to avoid exception being raised.
219 if not '--use-existing-results' in args:
220 args.append('--use-existing-results')
221
222 # Make sure autoserv running in container using a different pid file.
223 if not '--pidfile-label' in args:
224 args.extend(['--pidfile-label', 'container_autoserv'])
225
Dan Shid1f51232015-04-18 00:29:14 -0700226 cmd_line = ' '.join(["'%s'" % arg if ' ' in arg else arg for arg in args])
Dan Shicf4d2032015-03-12 15:04:21 -0700227 logging.info('Run command in container: %s', cmd_line)
Dan Shi37bee222015-04-13 15:46:47 -0700228 success = False
Dan Shicf4d2032015-03-12 15:04:21 -0700229 try:
230 test_container.attach_run(cmd_line)
Dan Shi37bee222015-04-13 15:46:47 -0700231 success = True
Dan Shi9d3454e2015-12-08 09:16:08 -0800232 except Exception as e:
233 # If the test run inside container fails without generating any log,
234 # write a message to status.log to help troubleshooting.
235 debug_files = os.listdir(os.path.join(results, 'debug'))
236 if not debug_files:
237 job.record('FAIL', None, None,
238 'Failed to run test inside the container: %s. Check '
239 'logs in ssp_logs folder for more details.' % e)
240 raise
Dan Shicf4d2032015-03-12 15:04:21 -0700241 finally:
Dan Shi37bee222015-04-13 15:46:47 -0700242 counter_key = '%s.%s' % (lxc.STATS_KEY,
243 'success' if success else 'fail')
244 autotest_stats.Counter(counter_key).increment()
245 # metadata is uploaded separately so it can use http to upload.
246 metadata = {'drone': socket.gethostname(),
247 'job_id': job_id,
248 'success': success}
249 autotest_es.post(use_http=True,
250 type_str=lxc.CONTAINER_RUN_TEST_METADB_TYPE,
251 metadata=metadata)
Dan Shicf4d2032015-03-12 15:04:21 -0700252 test_container.destroy()
253
254
Dan Shi3f1b8a52015-04-21 11:11:06 -0700255def correct_results_folder_permission(results):
256 """Make sure the results folder has the right permission settings.
257
258 For tests running with server-side packaging, the results folder has the
259 owner of root. This must be changed to the user running the autoserv
260 process, so parsing job can access the results folder.
261 TODO(dshi): crbug.com/459344 Remove this function when test container can be
262 unprivileged container.
263
264 @param results: Path to the results folder.
265
266 """
267 if not results:
268 return
269
Dan Shi32649b82015-08-29 20:53:36 -0700270 try:
271 utils.run('sudo -n chown -R %s "%s"' % (os.getuid(), results))
272 utils.run('sudo -n chgrp -R %s "%s"' % (os.getgid(), results))
273 except error.CmdError as e:
274 metadata = {'error': str(e),
275 'result_folder': results,
276 'drone': socket.gethostname()}
277 autotest_es.post(use_http=True, type_str='correct_results_folder_failure',
278 metadata=metadata)
279 raise
Dan Shi3f1b8a52015-04-21 11:11:06 -0700280
281
Dan Shia06f3e22015-09-03 16:15:15 -0700282def _start_servod(machine):
283 """Try to start servod in moblab if it's not already running or running with
284 different board or port.
285
286 @param machine: Name of the dut used for test.
287 """
288 if not utils.is_moblab():
289 return
290
Dan Shi1cded882015-09-23 16:52:26 -0700291 logging.debug('Trying to start servod.')
Dan Shia06f3e22015-09-03 16:15:15 -0700292 try:
Kevin Cheng9b6930f2016-07-20 14:57:15 -0700293 afe = frontend.AFE()
Dan Shia06f3e22015-09-03 16:15:15 -0700294 board = server_utils.get_board_from_afe(machine, afe)
295 hosts = afe.get_hosts(hostname=machine)
296 servo_host = hosts[0].attributes.get('servo_host', None)
297 servo_port = hosts[0].attributes.get('servo_port', 9999)
298 if not servo_host in ['localhost', '127.0.0.1']:
Dan Shi1cded882015-09-23 16:52:26 -0700299 logging.warn('Starting servod is aborted. The dut\'s servo_host '
300 'attribute is not set to localhost.')
Dan Shia06f3e22015-09-03 16:15:15 -0700301 return
302 except (urllib2.HTTPError, urllib2.URLError):
303 # Ignore error if RPC failed to get board
304 logging.error('Failed to get board name from AFE. Start servod is '
305 'aborted')
306 return
307
308 try:
309 pid = utils.run('pgrep servod').stdout
310 cmd_line = utils.run('ps -fp %s' % pid).stdout
311 if ('--board %s' % board in cmd_line and
312 '--port %s' % servo_port in cmd_line):
313 logging.debug('Servod is already running with given board and port.'
314 ' There is no need to restart servod.')
315 return
316 logging.debug('Servod is running with different board or port. '
317 'Stopping existing servod.')
318 utils.run('sudo stop servod')
319 except error.CmdError:
320 # servod is not running.
321 pass
322
323 try:
324 utils.run(START_SERVOD_CMD % (board, servo_port))
325 logging.debug('Servod is started')
326 except error.CmdError as e:
327 logging.error('Servod failed to be started, error: %s', e)
328
329
Dan Shic68fefb2015-04-07 10:10:52 -0700330def run_autoserv(pid_file_manager, results, parser, ssp_url, use_ssp):
Dan Shicf4d2032015-03-12 15:04:21 -0700331 """Run server job with given options.
332
333 @param pid_file_manager: PidFileManager used to monitor the autoserv process
334 @param results: Folder to store results.
335 @param parser: Parser for the command line arguments.
336 @param ssp_url: Url to server-side package.
Dan Shic68fefb2015-04-07 10:10:52 -0700337 @param use_ssp: Set to True to run with server-side packaging.
Dan Shicf4d2032015-03-12 15:04:21 -0700338 """
Dan Shiec1d47d2015-02-13 11:38:13 -0800339 if parser.options.warn_no_ssp:
Dan Shic68fefb2015-04-07 10:10:52 -0700340 # Post a warning in the log.
Dan Shiec1d47d2015-02-13 11:38:13 -0800341 logging.warn('Autoserv is required to run with server-side packaging. '
342 'However, no drone is found to support server-side '
343 'packaging. The test will be executed in a drone without '
344 'server-side packaging supported.')
345
jadmanski0afbb632008-06-06 21:10:57 +0000346 # send stdin to /dev/null
347 dev_null = os.open(os.devnull, os.O_RDONLY)
348 os.dup2(dev_null, sys.stdin.fileno())
349 os.close(dev_null)
mblighdbf37612007-11-24 19:38:11 +0000350
Dan Shie8aeb662016-06-30 11:22:03 -0700351 # Create separate process group if the process is not a process group
352 # leader. This allows autoserv process to keep running after the caller
353 # process (drone manager call) exits.
354 if os.getpid() != os.getpgid(0):
355 os.setsid()
mbligh1d42d4e2007-11-05 22:42:00 +0000356
Dan Shicf4d2032015-03-12 15:04:21 -0700357 # Container name is predefined so the container can be destroyed in
358 # handle_sigterm.
359 job_or_task_id = job_directories.get_job_id_or_task_id(
360 parser.options.results)
361 container_name = (lxc.TEST_CONTAINER_NAME_FMT %
Dan Shid68d51c2015-04-21 17:00:42 -0700362 (job_or_task_id, time.time(), os.getpid()))
Dan Shiafa63872016-02-23 15:32:31 -0800363 job_folder = job_directories.get_job_folder_name(parser.options.results)
Dan Shicf4d2032015-03-12 15:04:21 -0700364
jadmanski0afbb632008-06-06 21:10:57 +0000365 # Implement SIGTERM handler
mblighc2299562009-07-02 19:00:36 +0000366 def handle_sigterm(signum, frame):
Simran Basi9d9b7292013-10-16 16:44:07 -0700367 logging.debug('Received SIGTERM')
mblighff7d61f2008-12-22 14:53:35 +0000368 if pid_file_manager:
369 pid_file_manager.close_file(1, signal.SIGTERM)
Simran Basi49e21e62013-10-17 12:40:33 -0700370 logging.debug('Finished writing to pid_file. Killing process.')
Dan Shi3f1b8a52015-04-21 11:11:06 -0700371
372 # Update results folder's file permission. This needs to be done ASAP
373 # before the parsing process tries to access the log.
374 if use_ssp and results:
375 correct_results_folder_permission(results)
376
Simran Basid6b83772014-01-06 16:31:30 -0800377 # TODO (sbasi) - remove the time.sleep when crbug.com/302815 is solved.
378 # This sleep allows the pending output to be logged before the kill
379 # signal is sent.
380 time.sleep(.1)
Dan Shic68fefb2015-04-07 10:10:52 -0700381 if use_ssp:
Dan Shicf4d2032015-03-12 15:04:21 -0700382 logging.debug('Destroy container %s before aborting the autoserv '
383 'process.', container_name)
Dan Shi3f1b8a52015-04-21 11:11:06 -0700384 metadata = {'drone': socket.gethostname(),
385 'job_id': job_or_task_id,
386 'container_name': container_name,
387 'action': 'abort',
388 'success': True}
Dan Shicf4d2032015-03-12 15:04:21 -0700389 try:
390 bucket = lxc.ContainerBucket()
391 container = bucket.get(container_name)
392 if container:
393 container.destroy()
394 else:
Dan Shi3f1b8a52015-04-21 11:11:06 -0700395 metadata['success'] = False
396 metadata['error'] = 'container not found'
Dan Shicf4d2032015-03-12 15:04:21 -0700397 logging.debug('Container %s is not found.', container_name)
398 except:
Dan Shi3f1b8a52015-04-21 11:11:06 -0700399 metadata['success'] = False
400 metadata['error'] = 'Exception: %s' % sys.exc_info()
Dan Shicf4d2032015-03-12 15:04:21 -0700401 # Handle any exception so the autoserv process can be aborted.
402 logging.error('Failed to destroy container %s. Error: %s',
403 container_name, sys.exc_info())
Dan Shi3f1b8a52015-04-21 11:11:06 -0700404 autotest_es.post(use_http=True,
405 type_str=lxc.CONTAINER_RUN_TEST_METADB_TYPE,
406 metadata=metadata)
Dan Shie4a4f9f2015-07-20 09:00:25 -0700407 # Try to correct the result file permission again after the
408 # container is destroyed, as the container might have created some
409 # new files in the result folder.
410 if results:
411 correct_results_folder_permission(results)
Dan Shicf4d2032015-03-12 15:04:21 -0700412
jadmanski0afbb632008-06-06 21:10:57 +0000413 os.killpg(os.getpgrp(), signal.SIGKILL)
mblighfaf0cd42007-11-19 16:00:24 +0000414
jadmanski0afbb632008-06-06 21:10:57 +0000415 # Set signal handler
mblighc2299562009-07-02 19:00:36 +0000416 signal.signal(signal.SIGTERM, handle_sigterm)
mbligha46678d2008-05-01 20:00:01 +0000417
Simran Basid6b83772014-01-06 16:31:30 -0800418 # faulthandler is only needed to debug in the Lab and is not avaliable to
419 # be imported in the chroot as part of VMTest, so Try-Except it.
420 try:
421 import faulthandler
422 faulthandler.register(signal.SIGTERM, all_threads=True, chain=True)
423 logging.debug('faulthandler registered on SIGTERM.')
424 except ImportError:
Christopher Grant4beca022015-06-16 15:14:47 -0400425 sys.exc_clear()
Simran Basid6b83772014-01-06 16:31:30 -0800426
David Rochberg8a60d1e2011-02-01 14:22:07 -0500427 # Ignore SIGTTOU's generated by output from forked children.
428 signal.signal(signal.SIGTTOU, signal.SIG_IGN)
429
Alex Millerf1af17e2013-01-09 22:50:32 -0800430 # If we received a SIGALARM, let's be loud about it.
431 signal.signal(signal.SIGALRM, log_alarm)
432
mbligha5f5e542009-12-30 16:57:49 +0000433 # Server side tests that call shell scripts often depend on $USER being set
434 # but depending on how you launch your autotest scheduler it may not be set.
435 os.environ['USER'] = getpass.getuser()
436
mblighb2bea302008-07-24 20:25:57 +0000437 label = parser.options.label
mbligh374f3412009-05-13 21:29:45 +0000438 group_name = parser.options.group_name
mblighb2bea302008-07-24 20:25:57 +0000439 user = parser.options.user
440 client = parser.options.client
441 server = parser.options.server
jadmanski0afbb632008-06-06 21:10:57 +0000442 install_before = parser.options.install_before
mblighb2bea302008-07-24 20:25:57 +0000443 install_after = parser.options.install_after
444 verify = parser.options.verify
445 repair = parser.options.repair
showard45ae8192008-11-05 19:32:53 +0000446 cleanup = parser.options.cleanup
Alex Millercb79ba72013-05-29 14:43:00 -0700447 provision = parser.options.provision
Dan Shi07e09af2013-04-12 09:31:29 -0700448 reset = parser.options.reset
Alex Miller667b5f22014-02-28 15:33:39 -0800449 job_labels = parser.options.job_labels
mblighb2bea302008-07-24 20:25:57 +0000450 no_tee = parser.options.no_tee
jadmanski0afbb632008-06-06 21:10:57 +0000451 parse_job = parser.options.parse_job
mblighe7d9c602009-07-02 19:02:33 +0000452 execution_tag = parser.options.execution_tag
453 if not execution_tag:
454 execution_tag = parse_job
jadmanski0afbb632008-06-06 21:10:57 +0000455 ssh_user = parser.options.ssh_user
456 ssh_port = parser.options.ssh_port
457 ssh_pass = parser.options.ssh_pass
jadmanskidef0c3c2009-03-25 20:07:10 +0000458 collect_crashinfo = parser.options.collect_crashinfo
mblighe0cbc912010-03-11 18:03:07 +0000459 control_filename = parser.options.control_filename
Scott Zawalski91493c82013-01-25 16:15:20 -0500460 test_retry = parser.options.test_retry
beepscb6f1e22013-06-28 19:14:10 -0700461 verify_job_repo_url = parser.options.verify_job_repo_url
Christopher Wileyf594c5e2013-07-03 18:25:30 -0700462 skip_crash_collection = parser.options.skip_crash_collection
Aviv Keshet18ee3142013-08-12 15:01:51 -0700463 ssh_verbosity = int(parser.options.ssh_verbosity)
Fang Deng6cc20de2013-09-06 15:47:32 -0700464 ssh_options = parser.options.ssh_options
Dan Shib669cbd2013-09-13 11:17:17 -0700465 no_use_packaging = parser.options.no_use_packaging
Simran Basi1bf60eb2015-12-01 16:39:29 -0800466 host_attributes = parser.options.host_attributes
467 in_lab = bool(parser.options.lab)
mbligha46678d2008-05-01 20:00:01 +0000468
mblighb2bea302008-07-24 20:25:57 +0000469 # can't be both a client and a server side test
470 if client and server:
Eric Li861b2d52011-02-04 14:50:35 -0800471 parser.parser.error("Can not specify a test as both server and client!")
mblighb2bea302008-07-24 20:25:57 +0000472
Alex Millercb79ba72013-05-29 14:43:00 -0700473 if provision and client:
474 parser.parser.error("Cannot specify provisioning and client!")
475
476 is_special_task = (verify or repair or cleanup or collect_crashinfo or
Dan Shi07e09af2013-04-12 09:31:29 -0700477 provision or reset)
Alex Millercb79ba72013-05-29 14:43:00 -0700478 if len(parser.args) < 1 and not is_special_task:
Eric Li861b2d52011-02-04 14:50:35 -0800479 parser.parser.error("Missing argument: control file")
mbligha46678d2008-05-01 20:00:01 +0000480
Aviv Keshet18ee3142013-08-12 15:01:51 -0700481 if ssh_verbosity > 0:
482 # ssh_verbosity is an integer between 0 and 3, inclusive
483 ssh_verbosity_flag = '-' + 'v' * ssh_verbosity
Fang Dengd1c2b732013-08-20 12:59:46 -0700484 else:
485 ssh_verbosity_flag = ''
Aviv Keshet18ee3142013-08-12 15:01:51 -0700486
showard45ae8192008-11-05 19:32:53 +0000487 # We have a control file unless it's just a verify/repair/cleanup job
jadmanski0afbb632008-06-06 21:10:57 +0000488 if len(parser.args) > 0:
489 control = parser.args[0]
490 else:
491 control = None
mbligha46678d2008-05-01 20:00:01 +0000492
Dan Shicf4d2032015-03-12 15:04:21 -0700493 machines = _get_machines(parser)
mbligh374f3412009-05-13 21:29:45 +0000494 if group_name and len(machines) < 2:
Dan Shicf4d2032015-03-12 15:04:21 -0700495 parser.parser.error('-G %r may only be supplied with more than one '
496 'machine.' % group_name)
mbligh374f3412009-05-13 21:29:45 +0000497
Christopher Wiley8a91f232013-07-09 11:02:27 -0700498 kwargs = {'group_name': group_name, 'tag': execution_tag,
Dan Shicf4d2032015-03-12 15:04:21 -0700499 'disable_sysinfo': parser.options.disable_sysinfo}
Dan Shi70647ca2015-07-16 22:52:35 -0700500 if parser.options.parent_job_id:
501 kwargs['parent_job_id'] = int(parser.options.parent_job_id)
mblighe0cbc912010-03-11 18:03:07 +0000502 if control_filename:
503 kwargs['control_filename'] = control_filename
Simran Basi1bf60eb2015-12-01 16:39:29 -0800504 if host_attributes:
505 kwargs['host_attributes'] = host_attributes
506 kwargs['in_lab'] = in_lab
jadmanski0afbb632008-06-06 21:10:57 +0000507 job = server_job.server_job(control, parser.args[1:], results, label,
508 user, machines, client, parse_job,
Fang Dengd1c2b732013-08-20 12:59:46 -0700509 ssh_user, ssh_port, ssh_pass,
Aviv Keshetc5947fa2013-09-04 14:06:29 -0700510 ssh_verbosity_flag, ssh_options,
511 test_retry, **kwargs)
Dan Shicf4d2032015-03-12 15:04:21 -0700512
showard75cdfee2009-06-10 17:40:41 +0000513 job.logging.start_logging()
mbligh4608b002010-01-05 18:22:35 +0000514 job.init_parser()
mbligha46678d2008-05-01 20:00:01 +0000515
mbligh161fe6f2008-06-19 16:26:04 +0000516 # perform checks
517 job.precheck()
518
jadmanski0afbb632008-06-06 21:10:57 +0000519 # run the job
520 exit_code = 0
Dan Shic1b8bdd2015-09-14 23:11:24 -0700521 auto_start_servod = _CONFIG.get_config_value(
522 'AUTOSERV', 'auto_start_servod', type=bool, default=False)
jadmanski0afbb632008-06-06 21:10:57 +0000523 try:
mbligh332000a2009-06-08 16:47:28 +0000524 try:
525 if repair:
Dan Shic1b8bdd2015-09-14 23:11:24 -0700526 if auto_start_servod and len(machines) == 1:
527 _start_servod(machines[0])
J. Richard Barnettec2d99cf2015-11-18 12:46:15 -0800528 job.repair(job_labels)
mbligh332000a2009-06-08 16:47:28 +0000529 elif verify:
Alex Miller667b5f22014-02-28 15:33:39 -0800530 job.verify(job_labels)
Alex Millercb79ba72013-05-29 14:43:00 -0700531 elif provision:
Alex Miller667b5f22014-02-28 15:33:39 -0800532 job.provision(job_labels)
Dan Shi07e09af2013-04-12 09:31:29 -0700533 elif reset:
Alex Miller667b5f22014-02-28 15:33:39 -0800534 job.reset(job_labels)
Fang Dengad78aca2014-10-02 18:15:46 -0700535 elif cleanup:
536 job.cleanup(job_labels)
mbligh332000a2009-06-08 16:47:28 +0000537 else:
Dan Shia06f3e22015-09-03 16:15:15 -0700538 if auto_start_servod and len(machines) == 1:
539 _start_servod(machines[0])
Dan Shic68fefb2015-04-07 10:10:52 -0700540 if use_ssp:
Dan Shicf4d2032015-03-12 15:04:21 -0700541 try:
Dan Shi37befda2015-12-07 13:16:56 -0800542 _run_with_ssp(job, container_name, job_or_task_id,
Dan Shiafa63872016-02-23 15:32:31 -0800543 results, parser, ssp_url, job_folder)
Dan Shicf4d2032015-03-12 15:04:21 -0700544 finally:
545 # Update the ownership of files in result folder.
Dan Shi3f1b8a52015-04-21 11:11:06 -0700546 correct_results_folder_permission(results)
Dan Shicf4d2032015-03-12 15:04:21 -0700547 else:
Dan Shiafa63872016-02-23 15:32:31 -0800548 if collect_crashinfo:
549 # Update the ownership of files in result folder. If the
550 # job to collect crashinfo was running inside container
551 # (SSP) and crashed before correcting folder permission,
552 # the result folder might have wrong permission setting.
553 try:
554 correct_results_folder_permission(results)
555 except:
556 # Ignore any error as the user may not have root
557 # permission to run sudo command.
558 pass
Dan Shicf4d2032015-03-12 15:04:21 -0700559 job.run(install_before, install_after,
560 verify_job_repo_url=verify_job_repo_url,
561 only_collect_crashinfo=collect_crashinfo,
562 skip_crash_collection=skip_crash_collection,
Dan Shib669cbd2013-09-13 11:17:17 -0700563 job_labels=job_labels,
564 use_packaging=(not no_use_packaging))
mbligh332000a2009-06-08 16:47:28 +0000565 finally:
566 while job.hosts:
567 host = job.hosts.pop()
568 host.close()
jadmanski0afbb632008-06-06 21:10:57 +0000569 except:
jadmanski27b37ea2008-10-29 23:54:31 +0000570 exit_code = 1
jadmanski0afbb632008-06-06 21:10:57 +0000571 traceback.print_exc()
mbligha46678d2008-05-01 20:00:01 +0000572
mblighff7d61f2008-12-22 14:53:35 +0000573 if pid_file_manager:
574 pid_file_manager.num_tests_failed = job.num_tests_failed
575 pid_file_manager.close_file(exit_code)
jadmanskie0dffc32008-12-15 17:30:30 +0000576 job.cleanup_parser()
showard21baa452008-10-21 00:08:39 +0000577
jadmanski27b37ea2008-10-29 23:54:31 +0000578 sys.exit(exit_code)
mbligha46678d2008-05-01 20:00:01 +0000579
580
Fang Deng042c1472014-10-23 13:56:41 -0700581def record_autoserv(options, duration_secs):
582 """Record autoserv end-to-end time in metadata db.
583
584 @param options: parser options.
585 @param duration_secs: How long autoserv has taken, in secs.
586 """
587 # Get machine hostname
588 machines = options.machines.replace(
589 ',', ' ').strip().split() if options.machines else []
590 num_machines = len(machines)
591 if num_machines > 1:
592 # Skip the case where atomic group is used.
593 return
594 elif num_machines == 0:
595 machines.append('hostless')
596
597 # Determine the status that will be reported.
598 s = job_overhead.STATUS
599 task_mapping = {
600 'reset': s.RESETTING, 'verify': s.VERIFYING,
601 'provision': s.PROVISIONING, 'repair': s.REPAIRING,
602 'cleanup': s.CLEANING, 'collect_crashinfo': s.GATHERING}
Dan Shi888cfca2015-07-31 15:49:00 -0700603 match = filter(lambda task: getattr(options, task, False) == True,
604 task_mapping)
Fang Deng042c1472014-10-23 13:56:41 -0700605 status = task_mapping[match[0]] if match else s.RUNNING
606 is_special_task = status not in [s.RUNNING, s.GATHERING]
Dan Shicf4d2032015-03-12 15:04:21 -0700607 job_or_task_id = job_directories.get_job_id_or_task_id(options.results)
Fang Deng042c1472014-10-23 13:56:41 -0700608 job_overhead.record_state_duration(
609 job_or_task_id, machines[0], status, duration_secs,
610 is_special_task=is_special_task)
611
612
mbligha46678d2008-05-01 20:00:01 +0000613def main():
Fang Deng042c1472014-10-23 13:56:41 -0700614 start_time = datetime.datetime.now()
Dan Shia1ecd5c2013-06-06 11:21:31 -0700615 # White list of tests with run time measurement enabled.
Dan Shia06f3e22015-09-03 16:15:15 -0700616 measure_run_time_tests_names = _CONFIG.get_config_value(
617 'AUTOSERV', 'measure_run_time_tests', type=str)
Dan Shia1ecd5c2013-06-06 11:21:31 -0700618 if measure_run_time_tests_names:
619 measure_run_time_tests = [t.strip() for t in
620 measure_run_time_tests_names.split(',')]
621 else:
622 measure_run_time_tests = []
jadmanski0afbb632008-06-06 21:10:57 +0000623 # grab the parser
624 parser = autoserv_parser.autoserv_parser
mbligha5cb4062009-02-17 15:53:39 +0000625 parser.parse_args()
mbligha46678d2008-05-01 20:00:01 +0000626
jadmanski0afbb632008-06-06 21:10:57 +0000627 if len(sys.argv) == 1:
628 parser.parser.print_help()
629 sys.exit(1)
mbligha6f13082008-06-05 23:53:46 +0000630
Dan Shicf4d2032015-03-12 15:04:21 -0700631 # If the job requires to run with server-side package, try to stage server-
632 # side package first. If that fails with error that autotest server package
Dan Shic68fefb2015-04-07 10:10:52 -0700633 # does not exist, fall back to run the job without using server-side
634 # packaging. If option warn_no_ssp is specified, that means autoserv is
635 # running in a drone does not support SSP, thus no need to stage server-side
636 # package.
Dan Shicf4d2032015-03-12 15:04:21 -0700637 ssp_url = None
Dan Shi0b754c52015-04-20 14:20:38 -0700638 ssp_url_warning = False
Dan Shic68fefb2015-04-07 10:10:52 -0700639 if (not parser.options.warn_no_ssp and parser.options.require_ssp):
Dan Shi14de7622016-08-22 11:09:06 -0700640 ssp_url, ssp_error_msg = _stage_ssp(parser)
Dan Shi0b754c52015-04-20 14:20:38 -0700641 # The build does not have autotest server package. Fall back to not
642 # to use server-side package. Logging is postponed until logging being
643 # set up.
644 ssp_url_warning = not ssp_url
Dan Shicf4d2032015-03-12 15:04:21 -0700645
showard75cdfee2009-06-10 17:40:41 +0000646 if parser.options.no_logging:
647 results = None
648 else:
649 results = parser.options.results
mbligh80e1eba2008-11-19 00:26:18 +0000650 if not results:
651 results = 'results.' + time.strftime('%Y-%m-%d-%H.%M.%S')
Dan Shi14de7622016-08-22 11:09:06 -0700652 results = os.path.abspath(results)
showard566d3c02010-01-12 18:57:01 +0000653 resultdir_exists = False
654 for filename in ('control.srv', 'status.log', '.autoserv_execute'):
655 if os.path.exists(os.path.join(results, filename)):
656 resultdir_exists = True
mbligh4608b002010-01-05 18:22:35 +0000657 if not parser.options.use_existing_results and resultdir_exists:
mbligh80e1eba2008-11-19 00:26:18 +0000658 error = "Error: results directory already exists: %s\n" % results
659 sys.stderr.write(error)
660 sys.exit(1)
mbligha788dc42009-03-26 21:10:16 +0000661
662 # Now that we certified that there's no leftover results dir from
663 # previous jobs, lets create the result dir since the logging system
664 # needs to create the log file in there.
665 if not os.path.isdir(results):
666 os.makedirs(results)
showard75cdfee2009-06-10 17:40:41 +0000667
Dan Shic68fefb2015-04-07 10:10:52 -0700668 # Server-side packaging will only be used if it's required and the package
669 # is available. If warn_no_ssp is specified, it means that autoserv is
670 # running in a drone does not have SSP supported and a warning will be logs.
671 # Therefore, it should not run with SSP.
672 use_ssp = (not parser.options.warn_no_ssp and parser.options.require_ssp
673 and ssp_url)
674 if use_ssp:
Dan Shie28de552015-05-06 16:51:58 -0700675 log_dir = os.path.join(results, 'ssp_logs') if results else None
Dan Shicf4d2032015-03-12 15:04:21 -0700676 if log_dir and not os.path.exists(log_dir):
677 os.makedirs(log_dir)
678 else:
679 log_dir = results
Dan Shi3f1b8a52015-04-21 11:11:06 -0700680
showard75cdfee2009-06-10 17:40:41 +0000681 logging_manager.configure_logging(
Dan Shicf4d2032015-03-12 15:04:21 -0700682 server_logging_config.ServerLoggingConfig(),
683 results_dir=log_dir,
showard10d84172009-06-18 23:16:50 +0000684 use_console=not parser.options.no_tee,
685 verbose=parser.options.verbose,
686 no_console_prefix=parser.options.no_console_prefix)
Dan Shicf4d2032015-03-12 15:04:21 -0700687
Dan Shi0b754c52015-04-20 14:20:38 -0700688 if ssp_url_warning:
689 logging.warn(
690 'Autoserv is required to run with server-side packaging. '
691 'However, no server-side package can be found based on '
Dan Shi6450e142016-03-11 11:52:20 -0800692 '`--image`, host attribute job_repo_url or host OS version '
693 'label. It could be that the build to test is older than the '
694 'minimum version that supports server-side packaging. The test '
Dan Shi14de7622016-08-22 11:09:06 -0700695 'will be executed without using erver-side packaging. '
696 'Following is the detailed error:\n%s', ssp_error_msg)
Dan Shi0b754c52015-04-20 14:20:38 -0700697
showard75cdfee2009-06-10 17:40:41 +0000698 if results:
mbligha788dc42009-03-26 21:10:16 +0000699 logging.info("Results placed in %s" % results)
mbligh10717632008-11-19 00:21:57 +0000700
mbligh4608b002010-01-05 18:22:35 +0000701 # wait until now to perform this check, so it get properly logged
Dan Shicf4d2032015-03-12 15:04:21 -0700702 if (parser.options.use_existing_results and not resultdir_exists and
Dan Shiff78f112015-06-12 13:34:02 -0700703 not utils.is_in_container()):
mbligh4608b002010-01-05 18:22:35 +0000704 logging.error("No existing results directory found: %s", results)
705 sys.exit(1)
706
Dan Shicf4d2032015-03-12 15:04:21 -0700707 logging.debug('autoserv is running in drone %s.', socket.gethostname())
Aviv Keshet5c40ec62013-08-20 12:11:12 -0700708 logging.debug('autoserv command was: %s', ' '.join(sys.argv))
mbligh4608b002010-01-05 18:22:35 +0000709
Dan Shicf4d2032015-03-12 15:04:21 -0700710 if parser.options.write_pidfile and results:
mbligh4608b002010-01-05 18:22:35 +0000711 pid_file_manager = pidfile.PidFileManager(parser.options.pidfile_label,
712 results)
jadmanskid5ab8c52008-12-03 16:27:07 +0000713 pid_file_manager.open_file()
mblighff7d61f2008-12-22 14:53:35 +0000714 else:
715 pid_file_manager = None
mbligha46678d2008-05-01 20:00:01 +0000716
jadmanskif22fea82008-11-26 20:57:07 +0000717 autotest.BaseAutotest.set_install_in_tmpdir(
718 parser.options.install_in_tmpdir)
719
Dan Shia1ecd5c2013-06-06 11:21:31 -0700720 timer = None
721 try:
722 # Take the first argument as control file name, get the test name from
723 # the control file. If the test name exists in the list of tests with
724 # run time measurement enabled, start a timer to begin measurement.
725 if (len(parser.args) > 0 and parser.args[0] != '' and
726 parser.options.machines):
Dan Shibbc16132013-07-09 16:23:59 -0700727 try:
728 test_name = control_data.parse_control(parser.args[0],
729 raise_warnings=True).name
730 except control_data.ControlVariableException:
731 logging.debug('Failed to retrieve test name from control file.')
732 test_name = None
Dan Shia1ecd5c2013-06-06 11:21:31 -0700733 if test_name in measure_run_time_tests:
734 machines = parser.options.machines.replace(',', ' '
735 ).strip().split()
Dan Shi8eac5af2014-09-17 00:15:15 -0700736 try:
Kevin Cheng9b6930f2016-07-20 14:57:15 -0700737 afe = frontend.AFE()
Dan Shi8eac5af2014-09-17 00:15:15 -0700738 board = server_utils.get_board_from_afe(machines[0], afe)
Gabe Black1e1c41b2015-02-04 23:55:15 -0800739 timer = autotest_stats.Timer('autoserv_run_time.%s.%s' %
740 (board, test_name))
Dan Shi8eac5af2014-09-17 00:15:15 -0700741 timer.start()
742 except (urllib2.HTTPError, urllib2.URLError):
743 # Ignore error if RPC failed to get board
744 pass
Dan Shia1ecd5c2013-06-06 11:21:31 -0700745 except control_data.ControlVariableException as e:
746 logging.error(str(e))
jadmanski0afbb632008-06-06 21:10:57 +0000747 exit_code = 0
Prashanth B6285f6a2014-05-08 18:01:27 -0700748 # TODO(beeps): Extend this to cover different failure modes.
749 # Testing exceptions are matched against labels sent to autoserv. Eg,
750 # to allow only the hostless job to run, specify
751 # testing_exceptions: test_suite in the shadow_config. To allow both
752 # the hostless job and dummy_Pass to run, specify
753 # testing_exceptions: test_suite,dummy_Pass. You can figure out
754 # what label autoserv is invoked with by looking through the logs of a test
755 # for the autoserv command's -l option.
Dan Shia06f3e22015-09-03 16:15:15 -0700756 testing_exceptions = _CONFIG.get_config_value(
Prashanth B6285f6a2014-05-08 18:01:27 -0700757 'AUTOSERV', 'testing_exceptions', type=list, default=[])
Dan Shia06f3e22015-09-03 16:15:15 -0700758 test_mode = _CONFIG.get_config_value(
Prashanth B6285f6a2014-05-08 18:01:27 -0700759 'AUTOSERV', 'testing_mode', type=bool, default=False)
Prashanth Balasubramanianf8b83712014-11-06 15:58:21 -0800760 test_mode = (results_mocker and test_mode and not
761 any([ex in parser.options.label
762 for ex in testing_exceptions]))
763 is_task = (parser.options.verify or parser.options.repair or
764 parser.options.provision or parser.options.reset or
765 parser.options.cleanup or parser.options.collect_crashinfo)
jadmanski0afbb632008-06-06 21:10:57 +0000766 try:
767 try:
Prashanth B6285f6a2014-05-08 18:01:27 -0700768 if test_mode:
Prashanth Balasubramanianf8b83712014-11-06 15:58:21 -0800769 # The parser doesn't run on tasks anyway, so we can just return
770 # happy signals without faking results.
771 if not is_task:
772 machine = parser.options.results.split('/')[-1]
773
774 # TODO(beeps): The proper way to do this would be to
775 # refactor job creation so we can invoke job.record
776 # directly. To do that one needs to pipe the test_name
777 # through run_autoserv and bail just before invoking
778 # the server job. See the comment in
779 # puppylab/results_mocker for more context.
780 results_mocker.ResultsMocker(
Prashanth Balasubramanian22dd2262014-11-28 18:19:18 -0800781 test_name if test_name else 'unknown-test',
782 parser.options.results, machine
Prashanth Balasubramanianf8b83712014-11-06 15:58:21 -0800783 ).mock_results()
784 return
Prashanth B6285f6a2014-05-08 18:01:27 -0700785 else:
Dan Shic68fefb2015-04-07 10:10:52 -0700786 run_autoserv(pid_file_manager, results, parser, ssp_url,
787 use_ssp)
Aviv Keshet5c40ec62013-08-20 12:11:12 -0700788 except SystemExit as e:
jadmanski0afbb632008-06-06 21:10:57 +0000789 exit_code = e.code
Aviv Keshet5c40ec62013-08-20 12:11:12 -0700790 if exit_code:
791 logging.exception(e)
792 except Exception as e:
jadmanski0afbb632008-06-06 21:10:57 +0000793 # If we don't know what happened, we'll classify it as
794 # an 'abort' and return 1.
Aviv Keshet5c40ec62013-08-20 12:11:12 -0700795 logging.exception(e)
jadmanski0afbb632008-06-06 21:10:57 +0000796 exit_code = 1
797 finally:
mblighff7d61f2008-12-22 14:53:35 +0000798 if pid_file_manager:
799 pid_file_manager.close_file(exit_code)
Dan Shia1ecd5c2013-06-06 11:21:31 -0700800 if timer:
801 timer.stop()
Fang Deng042c1472014-10-23 13:56:41 -0700802 # Record the autoserv duration time. Must be called
803 # just before the system exits to ensure accuracy.
804 duration_secs = (datetime.datetime.now() - start_time).total_seconds()
805 record_autoserv(parser.options, duration_secs)
jadmanski0afbb632008-06-06 21:10:57 +0000806 sys.exit(exit_code)
mblighfaf0cd42007-11-19 16:00:24 +0000807
mblighbb421852008-03-11 22:36:16 +0000808
mbligha46678d2008-05-01 20:00:01 +0000809if __name__ == '__main__':
jadmanski0afbb632008-06-06 21:10:57 +0000810 main()