autoserv revision 77b79a6ee127ecefd3d6f783f81740900356b9fc
1#!/usr/bin/python -u 2# Copyright 2007-2008 Martin J. Bligh <mbligh@google.com>, Google Inc. 3# Released under the GPL v2 4 5""" 6Run a control file through the server side engine 7""" 8 9import ast 10import datetime 11import getpass 12import logging 13import os 14import re 15import signal 16import socket 17import sys 18import traceback 19import time 20import urllib2 21 22import common 23 24from autotest_lib.client.common_lib import control_data 25from autotest_lib.client.common_lib import global_config 26from autotest_lib.client.common_lib import utils 27from autotest_lib.client.common_lib.cros.graphite import autotest_es 28from autotest_lib.client.common_lib.cros.graphite import autotest_stats 29try: 30 from autotest_lib.puppylab import results_mocker 31except ImportError: 32 results_mocker = None 33 34require_atfork = global_config.global_config.get_config_value( 35 'AUTOSERV', 'require_atfork_module', type=bool, default=True) 36 37 38# Number of seconds to wait before returning if testing mode is enabled 39TESTING_MODE_SLEEP_SECS = 1 40 41try: 42 import atfork 43 atfork.monkeypatch_os_fork_functions() 44 import atfork.stdlib_fixer 45 # Fix the Python standard library for threading+fork safety with its 46 # internal locks. http://code.google.com/p/python-atfork/ 47 import warnings 48 warnings.filterwarnings('ignore', 'logging module already imported') 49 atfork.stdlib_fixer.fix_logging_module() 50except ImportError, e: 51 from autotest_lib.client.common_lib import global_config 52 if global_config.global_config.get_config_value( 53 'AUTOSERV', 'require_atfork_module', type=bool, default=False): 54 print >>sys.stderr, 'Please run utils/build_externals.py' 55 print e 56 sys.exit(1) 57 58from autotest_lib.server import frontend 59from autotest_lib.server import server_logging_config 60from autotest_lib.server import server_job, utils, autoserv_parser, autotest 61from autotest_lib.server import utils as server_utils 62from autotest_lib.site_utils import job_directories 63from autotest_lib.site_utils import job_overhead 64from autotest_lib.site_utils import lxc 65from autotest_lib.site_utils import lxc_utils 66from autotest_lib.client.common_lib import pidfile, logging_manager 67from autotest_lib.client.common_lib.cros.graphite import autotest_stats 68 69# Control segment to stage server-side package. 70STAGE_SERVER_SIDE_PACKAGE_CONTROL_FILE = server_job._control_segment_path( 71 'stage_server_side_package') 72 73def log_alarm(signum, frame): 74 logging.error("Received SIGALARM. Ignoring and continuing on.") 75 sys.exit(1) 76 77 78def _get_machines(parser): 79 """Get a list of machine names from command line arg -m or a file. 80 81 @param parser: Parser for the command line arguments. 82 83 @return: A list of machine names from command line arg -m or the 84 machines file specified in the command line arg -M. 85 """ 86 if parser.options.machines: 87 machines = parser.options.machines.replace(',', ' ').strip().split() 88 else: 89 machines = [] 90 machines_file = parser.options.machines_file 91 if machines_file: 92 machines = [] 93 for m in open(machines_file, 'r').readlines(): 94 # remove comments, spaces 95 m = re.sub('#.*', '', m).strip() 96 if m: 97 machines.append(m) 98 logging.debug('Read list of machines from file: %s', machines_file) 99 logging.debug('Machines: %s', ','.join(machines)) 100 101 if machines: 102 for machine in machines: 103 if not machine or re.search('\s', machine): 104 parser.parser.error("Invalid machine: %s" % str(machine)) 105 machines = list(set(machines)) 106 machines.sort() 107 return machines 108 109 110def _stage_ssp(parser): 111 """Stage server-side package. 112 113 This function calls a control segment to stage server-side package based on 114 the job and autoserv command line option. The detail implementation could 115 be different for each host type. Currently, only CrosHost has 116 stage_server_side_package function defined. 117 The script returns None if no server-side package is available. However, 118 it may raise exception if it failed for reasons other than artifact (the 119 server-side package) not found. 120 121 @param parser: Command line arguments parser passed in the autoserv process. 122 123 @return: url of the staged server-side package. Return None if server- 124 side package is not found for the build. 125 """ 126 # If test_source_build is not specified, default to use server-side test 127 # code from build specified in --image. 128 namespace = {'machines': _get_machines(parser), 129 'image': (parser.options.test_source_build or 130 parser.options.image),} 131 script_locals = {} 132 execfile(STAGE_SERVER_SIDE_PACKAGE_CONTROL_FILE, namespace, script_locals) 133 return script_locals['ssp_url'] 134 135 136def _run_with_ssp(container_name, job_id, results, parser, ssp_url): 137 """Run the server job with server-side packaging. 138 139 @param container_name: Name of the container to run the test. 140 @param job_id: ID of the test job. 141 @param results: Folder to store results. This could be different from 142 parser.options.results: 143 parser.options.results can be set to None for results to be 144 stored in a temp folder. 145 results can be None for autoserv run requires no logging. 146 @param parser: Command line parser that contains the options. 147 @param ssp_url: url of the staged server-side package. 148 """ 149 bucket = lxc.ContainerBucket() 150 control = (parser.args[0] if len(parser.args) > 0 and parser.args[0] != '' 151 else None) 152 test_container = bucket.setup_test(container_name, job_id, ssp_url, results, 153 control=control) 154 args = sys.argv[:] 155 args.remove('--require-ssp') 156 # --parent_job_id is only useful in autoserv running in host, not in 157 # container. Include this argument will cause test to fail for builds before 158 # CL 286265 was merged. 159 if '--parent_job_id' in args: 160 index = args.index('--parent_job_id') 161 args.remove('--parent_job_id') 162 # Remove the actual parent job id in command line arg. 163 del args[index] 164 165 # A dictionary of paths to replace in the command line. Key is the path to 166 # be replaced with the one in value. 167 paths_to_replace = {} 168 # Replace the control file path with the one in container. 169 if control: 170 container_control_filename = os.path.join( 171 lxc.CONTROL_TEMP_PATH, os.path.basename(control)) 172 paths_to_replace[control] = container_control_filename 173 # Update result directory with the one in container. 174 if parser.options.results: 175 container_result_dir = os.path.join(lxc.RESULT_DIR_FMT % job_id) 176 paths_to_replace[parser.options.results] = container_result_dir 177 # Update parse_job directory with the one in container. The assumption is 178 # that the result folder to be parsed is always the same as the results_dir. 179 if parser.options.parse_job: 180 container_parse_dir = os.path.join(lxc.RESULT_DIR_FMT % job_id) 181 paths_to_replace[parser.options.parse_job] = container_result_dir 182 183 args = [paths_to_replace.get(arg, arg) for arg in args] 184 185 # Apply --use-existing-results, results directory is aready created and 186 # mounted in container. Apply this arg to avoid exception being raised. 187 if not '--use-existing-results' in args: 188 args.append('--use-existing-results') 189 190 # Make sure autoserv running in container using a different pid file. 191 if not '--pidfile-label' in args: 192 args.extend(['--pidfile-label', 'container_autoserv']) 193 194 cmd_line = ' '.join(["'%s'" % arg if ' ' in arg else arg for arg in args]) 195 logging.info('Run command in container: %s', cmd_line) 196 success = False 197 try: 198 test_container.attach_run(cmd_line) 199 success = True 200 finally: 201 counter_key = '%s.%s' % (lxc.STATS_KEY, 202 'success' if success else 'fail') 203 autotest_stats.Counter(counter_key).increment() 204 # metadata is uploaded separately so it can use http to upload. 205 metadata = {'drone': socket.gethostname(), 206 'job_id': job_id, 207 'success': success} 208 autotest_es.post(use_http=True, 209 type_str=lxc.CONTAINER_RUN_TEST_METADB_TYPE, 210 metadata=metadata) 211 test_container.destroy() 212 213 214def correct_results_folder_permission(results): 215 """Make sure the results folder has the right permission settings. 216 217 For tests running with server-side packaging, the results folder has the 218 owner of root. This must be changed to the user running the autoserv 219 process, so parsing job can access the results folder. 220 TODO(dshi): crbug.com/459344 Remove this function when test container can be 221 unprivileged container. 222 223 @param results: Path to the results folder. 224 225 """ 226 if not results: 227 return 228 229 utils.run('sudo -n chown -R %s "%s"' % (os.getuid(), results)) 230 utils.run('sudo -n chgrp -R %s "%s"' % (os.getgid(), results)) 231 232 233def run_autoserv(pid_file_manager, results, parser, ssp_url, use_ssp): 234 """Run server job with given options. 235 236 @param pid_file_manager: PidFileManager used to monitor the autoserv process 237 @param results: Folder to store results. 238 @param parser: Parser for the command line arguments. 239 @param ssp_url: Url to server-side package. 240 @param use_ssp: Set to True to run with server-side packaging. 241 """ 242 if parser.options.warn_no_ssp: 243 # Post a warning in the log. 244 logging.warn('Autoserv is required to run with server-side packaging. ' 245 'However, no drone is found to support server-side ' 246 'packaging. The test will be executed in a drone without ' 247 'server-side packaging supported.') 248 249 # send stdin to /dev/null 250 dev_null = os.open(os.devnull, os.O_RDONLY) 251 os.dup2(dev_null, sys.stdin.fileno()) 252 os.close(dev_null) 253 254 # Create separate process group 255 os.setpgrp() 256 257 # Container name is predefined so the container can be destroyed in 258 # handle_sigterm. 259 job_or_task_id = job_directories.get_job_id_or_task_id( 260 parser.options.results) 261 container_name = (lxc.TEST_CONTAINER_NAME_FMT % 262 (job_or_task_id, time.time(), os.getpid())) 263 264 # Implement SIGTERM handler 265 def handle_sigterm(signum, frame): 266 logging.debug('Received SIGTERM') 267 if pid_file_manager: 268 pid_file_manager.close_file(1, signal.SIGTERM) 269 logging.debug('Finished writing to pid_file. Killing process.') 270 271 # Update results folder's file permission. This needs to be done ASAP 272 # before the parsing process tries to access the log. 273 if use_ssp and results: 274 correct_results_folder_permission(results) 275 276 # TODO (sbasi) - remove the time.sleep when crbug.com/302815 is solved. 277 # This sleep allows the pending output to be logged before the kill 278 # signal is sent. 279 time.sleep(.1) 280 if use_ssp: 281 logging.debug('Destroy container %s before aborting the autoserv ' 282 'process.', container_name) 283 metadata = {'drone': socket.gethostname(), 284 'job_id': job_or_task_id, 285 'container_name': container_name, 286 'action': 'abort', 287 'success': True} 288 try: 289 bucket = lxc.ContainerBucket() 290 container = bucket.get(container_name) 291 if container: 292 container.destroy() 293 else: 294 metadata['success'] = False 295 metadata['error'] = 'container not found' 296 logging.debug('Container %s is not found.', container_name) 297 except: 298 metadata['success'] = False 299 metadata['error'] = 'Exception: %s' % sys.exc_info() 300 # Handle any exception so the autoserv process can be aborted. 301 logging.error('Failed to destroy container %s. Error: %s', 302 container_name, sys.exc_info()) 303 autotest_es.post(use_http=True, 304 type_str=lxc.CONTAINER_RUN_TEST_METADB_TYPE, 305 metadata=metadata) 306 # Try to correct the result file permission again after the 307 # container is destroyed, as the container might have created some 308 # new files in the result folder. 309 if results: 310 correct_results_folder_permission(results) 311 312 os.killpg(os.getpgrp(), signal.SIGKILL) 313 314 # Set signal handler 315 signal.signal(signal.SIGTERM, handle_sigterm) 316 317 # faulthandler is only needed to debug in the Lab and is not avaliable to 318 # be imported in the chroot as part of VMTest, so Try-Except it. 319 try: 320 import faulthandler 321 faulthandler.register(signal.SIGTERM, all_threads=True, chain=True) 322 logging.debug('faulthandler registered on SIGTERM.') 323 except ImportError: 324 sys.exc_clear() 325 326 # Ignore SIGTTOU's generated by output from forked children. 327 signal.signal(signal.SIGTTOU, signal.SIG_IGN) 328 329 # If we received a SIGALARM, let's be loud about it. 330 signal.signal(signal.SIGALRM, log_alarm) 331 332 # Server side tests that call shell scripts often depend on $USER being set 333 # but depending on how you launch your autotest scheduler it may not be set. 334 os.environ['USER'] = getpass.getuser() 335 336 label = parser.options.label 337 group_name = parser.options.group_name 338 user = parser.options.user 339 client = parser.options.client 340 server = parser.options.server 341 install_before = parser.options.install_before 342 install_after = parser.options.install_after 343 verify = parser.options.verify 344 repair = parser.options.repair 345 cleanup = parser.options.cleanup 346 provision = parser.options.provision 347 reset = parser.options.reset 348 job_labels = parser.options.job_labels 349 no_tee = parser.options.no_tee 350 parse_job = parser.options.parse_job 351 execution_tag = parser.options.execution_tag 352 if not execution_tag: 353 execution_tag = parse_job 354 host_protection = parser.options.host_protection 355 ssh_user = parser.options.ssh_user 356 ssh_port = parser.options.ssh_port 357 ssh_pass = parser.options.ssh_pass 358 collect_crashinfo = parser.options.collect_crashinfo 359 control_filename = parser.options.control_filename 360 test_retry = parser.options.test_retry 361 verify_job_repo_url = parser.options.verify_job_repo_url 362 skip_crash_collection = parser.options.skip_crash_collection 363 ssh_verbosity = int(parser.options.ssh_verbosity) 364 ssh_options = parser.options.ssh_options 365 no_use_packaging = parser.options.no_use_packaging 366 367 # can't be both a client and a server side test 368 if client and server: 369 parser.parser.error("Can not specify a test as both server and client!") 370 371 if provision and client: 372 parser.parser.error("Cannot specify provisioning and client!") 373 374 is_special_task = (verify or repair or cleanup or collect_crashinfo or 375 provision or reset) 376 if len(parser.args) < 1 and not is_special_task: 377 parser.parser.error("Missing argument: control file") 378 379 if ssh_verbosity > 0: 380 # ssh_verbosity is an integer between 0 and 3, inclusive 381 ssh_verbosity_flag = '-' + 'v' * ssh_verbosity 382 else: 383 ssh_verbosity_flag = '' 384 385 # We have a control file unless it's just a verify/repair/cleanup job 386 if len(parser.args) > 0: 387 control = parser.args[0] 388 else: 389 control = None 390 391 machines = _get_machines(parser) 392 if group_name and len(machines) < 2: 393 parser.parser.error('-G %r may only be supplied with more than one ' 394 'machine.' % group_name) 395 396 kwargs = {'group_name': group_name, 'tag': execution_tag, 397 'disable_sysinfo': parser.options.disable_sysinfo} 398 if parser.options.parent_job_id: 399 kwargs['parent_job_id'] = int(parser.options.parent_job_id) 400 if control_filename: 401 kwargs['control_filename'] = control_filename 402 job = server_job.server_job(control, parser.args[1:], results, label, 403 user, machines, client, parse_job, 404 ssh_user, ssh_port, ssh_pass, 405 ssh_verbosity_flag, ssh_options, 406 test_retry, **kwargs) 407 408 job.logging.start_logging() 409 job.init_parser() 410 411 # perform checks 412 job.precheck() 413 414 # run the job 415 exit_code = 0 416 try: 417 try: 418 if repair: 419 job.repair(host_protection, job_labels) 420 elif verify: 421 job.verify(job_labels) 422 elif provision: 423 job.provision(job_labels) 424 elif reset: 425 job.reset(job_labels) 426 elif cleanup: 427 job.cleanup(job_labels) 428 else: 429 if use_ssp: 430 try: 431 _run_with_ssp(container_name, job_or_task_id, results, 432 parser, ssp_url) 433 finally: 434 # Update the ownership of files in result folder. 435 correct_results_folder_permission(results) 436 else: 437 job.run(install_before, install_after, 438 verify_job_repo_url=verify_job_repo_url, 439 only_collect_crashinfo=collect_crashinfo, 440 skip_crash_collection=skip_crash_collection, 441 job_labels=job_labels, 442 use_packaging=(not no_use_packaging)) 443 finally: 444 while job.hosts: 445 host = job.hosts.pop() 446 host.close() 447 except: 448 exit_code = 1 449 traceback.print_exc() 450 451 if pid_file_manager: 452 pid_file_manager.num_tests_failed = job.num_tests_failed 453 pid_file_manager.close_file(exit_code) 454 job.cleanup_parser() 455 456 sys.exit(exit_code) 457 458 459def record_autoserv(options, duration_secs): 460 """Record autoserv end-to-end time in metadata db. 461 462 @param options: parser options. 463 @param duration_secs: How long autoserv has taken, in secs. 464 """ 465 # Get machine hostname 466 machines = options.machines.replace( 467 ',', ' ').strip().split() if options.machines else [] 468 num_machines = len(machines) 469 if num_machines > 1: 470 # Skip the case where atomic group is used. 471 return 472 elif num_machines == 0: 473 machines.append('hostless') 474 475 # Determine the status that will be reported. 476 s = job_overhead.STATUS 477 task_mapping = { 478 'reset': s.RESETTING, 'verify': s.VERIFYING, 479 'provision': s.PROVISIONING, 'repair': s.REPAIRING, 480 'cleanup': s.CLEANING, 'collect_crashinfo': s.GATHERING} 481 # option_dict will be like {'reset': True, 'repair': False, ...} 482 option_dict = ast.literal_eval(str(options)) 483 match = filter(lambda task: option_dict.get(task) == True, task_mapping) 484 status = task_mapping[match[0]] if match else s.RUNNING 485 is_special_task = status not in [s.RUNNING, s.GATHERING] 486 job_or_task_id = job_directories.get_job_id_or_task_id(options.results) 487 job_overhead.record_state_duration( 488 job_or_task_id, machines[0], status, duration_secs, 489 is_special_task=is_special_task) 490 491 492def main(): 493 start_time = datetime.datetime.now() 494 # White list of tests with run time measurement enabled. 495 measure_run_time_tests_names = global_config.global_config.get_config_value( 496 'AUTOSERV', 'measure_run_time_tests', type=str) 497 if measure_run_time_tests_names: 498 measure_run_time_tests = [t.strip() for t in 499 measure_run_time_tests_names.split(',')] 500 else: 501 measure_run_time_tests = [] 502 # grab the parser 503 parser = autoserv_parser.autoserv_parser 504 parser.parse_args() 505 506 if len(sys.argv) == 1: 507 parser.parser.print_help() 508 sys.exit(1) 509 510 # If the job requires to run with server-side package, try to stage server- 511 # side package first. If that fails with error that autotest server package 512 # does not exist, fall back to run the job without using server-side 513 # packaging. If option warn_no_ssp is specified, that means autoserv is 514 # running in a drone does not support SSP, thus no need to stage server-side 515 # package. 516 ssp_url = None 517 ssp_url_warning = False 518 if (not parser.options.warn_no_ssp and parser.options.require_ssp): 519 ssp_url = _stage_ssp(parser) 520 # The build does not have autotest server package. Fall back to not 521 # to use server-side package. Logging is postponed until logging being 522 # set up. 523 ssp_url_warning = not ssp_url 524 525 if parser.options.no_logging: 526 results = None 527 else: 528 results = parser.options.results 529 if not results: 530 results = 'results.' + time.strftime('%Y-%m-%d-%H.%M.%S') 531 results = os.path.abspath(results) 532 resultdir_exists = False 533 for filename in ('control.srv', 'status.log', '.autoserv_execute'): 534 if os.path.exists(os.path.join(results, filename)): 535 resultdir_exists = True 536 if not parser.options.use_existing_results and resultdir_exists: 537 error = "Error: results directory already exists: %s\n" % results 538 sys.stderr.write(error) 539 sys.exit(1) 540 541 # Now that we certified that there's no leftover results dir from 542 # previous jobs, lets create the result dir since the logging system 543 # needs to create the log file in there. 544 if not os.path.isdir(results): 545 os.makedirs(results) 546 547 # Server-side packaging will only be used if it's required and the package 548 # is available. If warn_no_ssp is specified, it means that autoserv is 549 # running in a drone does not have SSP supported and a warning will be logs. 550 # Therefore, it should not run with SSP. 551 use_ssp = (not parser.options.warn_no_ssp and parser.options.require_ssp 552 and ssp_url) 553 if use_ssp: 554 log_dir = os.path.join(results, 'ssp_logs') if results else None 555 if log_dir and not os.path.exists(log_dir): 556 os.makedirs(log_dir) 557 else: 558 log_dir = results 559 560 logging_manager.configure_logging( 561 server_logging_config.ServerLoggingConfig(), 562 results_dir=log_dir, 563 use_console=not parser.options.no_tee, 564 verbose=parser.options.verbose, 565 no_console_prefix=parser.options.no_console_prefix) 566 567 if ssp_url_warning: 568 logging.warn( 569 'Autoserv is required to run with server-side packaging. ' 570 'However, no server-side package can be found based on ' 571 '`--image`, host attribute job_repo_url or host label of ' 572 'cros-version. The test will be executed without ' 573 'server-side packaging supported.') 574 575 if results: 576 logging.info("Results placed in %s" % results) 577 578 # wait until now to perform this check, so it get properly logged 579 if (parser.options.use_existing_results and not resultdir_exists and 580 not utils.is_in_container()): 581 logging.error("No existing results directory found: %s", results) 582 sys.exit(1) 583 584 logging.debug('autoserv is running in drone %s.', socket.gethostname()) 585 logging.debug('autoserv command was: %s', ' '.join(sys.argv)) 586 587 if parser.options.write_pidfile and results: 588 pid_file_manager = pidfile.PidFileManager(parser.options.pidfile_label, 589 results) 590 pid_file_manager.open_file() 591 else: 592 pid_file_manager = None 593 594 autotest.BaseAutotest.set_install_in_tmpdir( 595 parser.options.install_in_tmpdir) 596 597 timer = None 598 try: 599 # Take the first argument as control file name, get the test name from 600 # the control file. If the test name exists in the list of tests with 601 # run time measurement enabled, start a timer to begin measurement. 602 if (len(parser.args) > 0 and parser.args[0] != '' and 603 parser.options.machines): 604 try: 605 test_name = control_data.parse_control(parser.args[0], 606 raise_warnings=True).name 607 except control_data.ControlVariableException: 608 logging.debug('Failed to retrieve test name from control file.') 609 test_name = None 610 if test_name in measure_run_time_tests: 611 machines = parser.options.machines.replace(',', ' ' 612 ).strip().split() 613 try: 614 afe = frontend.AFE() 615 board = server_utils.get_board_from_afe(machines[0], afe) 616 timer = autotest_stats.Timer('autoserv_run_time.%s.%s' % 617 (board, test_name)) 618 timer.start() 619 except (urllib2.HTTPError, urllib2.URLError): 620 # Ignore error if RPC failed to get board 621 pass 622 except control_data.ControlVariableException as e: 623 logging.error(str(e)) 624 exit_code = 0 625 # TODO(beeps): Extend this to cover different failure modes. 626 # Testing exceptions are matched against labels sent to autoserv. Eg, 627 # to allow only the hostless job to run, specify 628 # testing_exceptions: test_suite in the shadow_config. To allow both 629 # the hostless job and dummy_Pass to run, specify 630 # testing_exceptions: test_suite,dummy_Pass. You can figure out 631 # what label autoserv is invoked with by looking through the logs of a test 632 # for the autoserv command's -l option. 633 testing_exceptions = global_config.global_config.get_config_value( 634 'AUTOSERV', 'testing_exceptions', type=list, default=[]) 635 test_mode = global_config.global_config.get_config_value( 636 'AUTOSERV', 'testing_mode', type=bool, default=False) 637 test_mode = (results_mocker and test_mode and not 638 any([ex in parser.options.label 639 for ex in testing_exceptions])) 640 is_task = (parser.options.verify or parser.options.repair or 641 parser.options.provision or parser.options.reset or 642 parser.options.cleanup or parser.options.collect_crashinfo) 643 try: 644 try: 645 if test_mode: 646 # The parser doesn't run on tasks anyway, so we can just return 647 # happy signals without faking results. 648 if not is_task: 649 machine = parser.options.results.split('/')[-1] 650 651 # TODO(beeps): The proper way to do this would be to 652 # refactor job creation so we can invoke job.record 653 # directly. To do that one needs to pipe the test_name 654 # through run_autoserv and bail just before invoking 655 # the server job. See the comment in 656 # puppylab/results_mocker for more context. 657 results_mocker.ResultsMocker( 658 test_name if test_name else 'unknown-test', 659 parser.options.results, machine 660 ).mock_results() 661 return 662 else: 663 run_autoserv(pid_file_manager, results, parser, ssp_url, 664 use_ssp) 665 except SystemExit as e: 666 exit_code = e.code 667 if exit_code: 668 logging.exception(e) 669 except Exception as e: 670 # If we don't know what happened, we'll classify it as 671 # an 'abort' and return 1. 672 logging.exception(e) 673 exit_code = 1 674 finally: 675 if pid_file_manager: 676 pid_file_manager.close_file(exit_code) 677 if timer: 678 timer.stop() 679 # Record the autoserv duration time. Must be called 680 # just before the system exits to ensure accuracy. 681 duration_secs = (datetime.datetime.now() - start_time).total_seconds() 682 record_autoserv(parser.options, duration_secs) 683 sys.exit(exit_code) 684 685 686if __name__ == '__main__': 687 main() 688