1# Copyright 2007 Google Inc. Released under the GPL v2 2#pylint: disable-msg=C0111 3 4import re, os, sys, traceback, time, glob, tempfile 5import logging 6from autotest_lib.server import installable_object, prebuild, utils 7from autotest_lib.client.common_lib import base_job, error, autotemp 8from autotest_lib.client.common_lib import base_packages, packages 9from autotest_lib.client.common_lib import global_config 10from autotest_lib.client.common_lib import utils as client_utils 11from autotest_lib.client.common_lib.cros.graphite import autotest_stats 12 13AUTOTEST_SVN = 'svn://test.kernel.org/autotest/trunk/client' 14AUTOTEST_HTTP = 'http://test.kernel.org/svn/autotest/trunk/client' 15 16 17get_value = global_config.global_config.get_config_value 18autoserv_prebuild = get_value('AUTOSERV', 'enable_server_prebuild', 19 type=bool, default=False) 20 21 22class AutodirNotFoundError(Exception): 23 """No Autotest installation could be found.""" 24 25 26class AutotestFailure(Exception): 27 """Gereric exception class for failures during a test run.""" 28 29 30class AutotestAbort(AutotestFailure): 31 """ 32 AutotestAborts are thrown when the DUT seems fine, 33 and the test doesn't give us an explicit reason for 34 failure; In this case we have no choice but to abort. 35 """ 36 37 38class AutotestDeviceError(AutotestFailure): 39 """ 40 Exceptions that inherit from AutotestDeviceError 41 are thrown when we can determine the current 42 state of the DUT and conclude that it probably 43 lead to the test failing; these exceptions lead 44 to failures instead of aborts. 45 """ 46 47 48class AutotestDeviceNotPingable(AutotestDeviceError): 49 """Error for when a DUT becomes unpingable.""" 50 51 52class AutotestDeviceNotSSHable(AutotestDeviceError): 53 """Error for when a DUT is pingable but not SSHable.""" 54 55 56class AutotestDeviceRebooted(AutotestDeviceError): 57 """Error for when a DUT rebooted unexpectedly.""" 58 59 60class BaseAutotest(installable_object.InstallableObject): 61 """ 62 This class represents the Autotest program. 63 64 Autotest is used to run tests automatically and collect the results. 65 It also supports profilers. 66 67 Implementation details: 68 This is a leaf class in an abstract class hierarchy, it must 69 implement the unimplemented methods in parent classes. 70 """ 71 72 def __init__(self, host=None): 73 self.host = host 74 self.got = False 75 self.installed = False 76 self.serverdir = utils.get_server_dir() 77 super(BaseAutotest, self).__init__() 78 79 80 install_in_tmpdir = False 81 @classmethod 82 def set_install_in_tmpdir(cls, flag): 83 """ Sets a flag that controls whether or not Autotest should by 84 default be installed in a "standard" directory (e.g. 85 /home/autotest, /usr/local/autotest) or a temporary directory. """ 86 cls.install_in_tmpdir = flag 87 88 89 @classmethod 90 def get_client_autodir_paths(cls, host): 91 return global_config.global_config.get_config_value( 92 'AUTOSERV', 'client_autodir_paths', type=list) 93 94 95 @classmethod 96 def get_installed_autodir(cls, host): 97 """ 98 Find where the Autotest client is installed on the host. 99 @returns an absolute path to an installed Autotest client root. 100 @raises AutodirNotFoundError if no Autotest installation can be found. 101 """ 102 autodir = host.get_autodir() 103 if autodir: 104 logging.debug('Using existing host autodir: %s', autodir) 105 return autodir 106 107 for path in Autotest.get_client_autodir_paths(host): 108 try: 109 autotest_binary = os.path.join(path, 'bin', 'autotest') 110 host.run('test -x %s' % utils.sh_escape(autotest_binary)) 111 host.run('test -w %s' % utils.sh_escape(path)) 112 logging.debug('Found existing autodir at %s', path) 113 return path 114 except error.AutoservRunError: 115 logging.debug('%s does not exist on %s', autotest_binary, 116 host.hostname) 117 raise AutodirNotFoundError 118 119 120 @classmethod 121 def get_install_dir(cls, host): 122 """ 123 Determines the location where autotest should be installed on 124 host. If self.install_in_tmpdir is set, it will return a unique 125 temporary directory that autotest can be installed in. Otherwise, looks 126 for an existing installation to use; if none is found, looks for a 127 usable directory in the global config client_autodir_paths. 128 """ 129 try: 130 install_dir = cls.get_installed_autodir(host) 131 except AutodirNotFoundError: 132 install_dir = cls._find_installable_dir(host) 133 134 if cls.install_in_tmpdir: 135 return host.get_tmp_dir(parent=install_dir) 136 return install_dir 137 138 139 @classmethod 140 def _find_installable_dir(cls, host): 141 client_autodir_paths = cls.get_client_autodir_paths(host) 142 for path in client_autodir_paths: 143 try: 144 host.run('mkdir -p %s' % utils.sh_escape(path)) 145 host.run('test -w %s' % utils.sh_escape(path)) 146 return path 147 except error.AutoservRunError: 148 logging.debug('Failed to create %s', path) 149 metadata = {'_type': 'AutoservInstallError', 150 'hostname': host.hostname} 151 autotest_stats.Counter('AutoservInstallError', 152 metadata=metadata).increment() 153 raise error.AutoservInstallError( 154 'Unable to find a place to install Autotest; tried %s' % 155 ', '.join(client_autodir_paths)) 156 157 158 def get_fetch_location(self): 159 c = global_config.global_config 160 repos = c.get_config_value("PACKAGES", 'fetch_location', type=list, 161 default=[]) 162 repos.reverse() 163 return repos 164 165 166 def install(self, host=None, autodir=None, use_packaging=True): 167 self._install(host=host, autodir=autodir, use_packaging=use_packaging) 168 169 170 def install_full_client(self, host=None, autodir=None): 171 self._install(host=host, autodir=autodir, use_autoserv=False, 172 use_packaging=False) 173 174 175 def install_no_autoserv(self, host=None, autodir=None): 176 self._install(host=host, autodir=autodir, use_autoserv=False) 177 178 179 def _install_using_packaging(self, host, autodir): 180 repos = self.get_fetch_location() 181 if not repos: 182 raise error.PackageInstallError("No repos to install an " 183 "autotest client from") 184 pkgmgr = packages.PackageManager(autodir, hostname=host.hostname, 185 repo_urls=repos, 186 do_locking=False, 187 run_function=host.run, 188 run_function_dargs=dict(timeout=600)) 189 # The packages dir is used to store all the packages that 190 # are fetched on that client. (for the tests,deps etc. 191 # too apart from the client) 192 pkg_dir = os.path.join(autodir, 'packages') 193 # clean up the autodir except for the packages directory 194 host.run('cd %s && ls | grep -v "^packages$"' 195 ' | xargs rm -rf && rm -rf .[!.]*' % autodir) 196 pkgmgr.install_pkg('autotest', 'client', pkg_dir, autodir, 197 preserve_install_dir=True) 198 self.installed = True 199 200 201 def _install_using_send_file(self, host, autodir): 202 dirs_to_exclude = set(["tests", "site_tests", "deps", "profilers", 203 "packages"]) 204 light_files = [os.path.join(self.source_material, f) 205 for f in os.listdir(self.source_material) 206 if f not in dirs_to_exclude] 207 host.send_file(light_files, autodir, delete_dest=True) 208 209 # create empty dirs for all the stuff we excluded 210 commands = [] 211 for path in dirs_to_exclude: 212 abs_path = os.path.join(autodir, path) 213 abs_path = utils.sh_escape(abs_path) 214 commands.append("mkdir -p '%s'" % abs_path) 215 commands.append("touch '%s'/__init__.py" % abs_path) 216 host.run(';'.join(commands)) 217 218 219 def _install(self, host=None, autodir=None, use_autoserv=True, 220 use_packaging=True): 221 """ 222 Install autotest. If get() was not called previously, an 223 attempt will be made to install from the autotest svn 224 repository. 225 226 @param host A Host instance on which autotest will be installed 227 @param autodir Location on the remote host to install to 228 @param use_autoserv Enable install modes that depend on the client 229 running with the autoserv harness 230 @param use_packaging Enable install modes that use the packaging system 231 232 @exception AutoservError if a tarball was not specified and 233 the target host does not have svn installed in its path 234 """ 235 if not host: 236 host = self.host 237 if not self.got: 238 self.get() 239 host.wait_up(timeout=30) 240 host.setup() 241 logging.info("Installing autotest on %s", host.hostname) 242 243 # set up the autotest directory on the remote machine 244 if not autodir: 245 autodir = self.get_install_dir(host) 246 logging.info('Using installation dir %s', autodir) 247 host.set_autodir(autodir) 248 host.run('mkdir -p %s' % utils.sh_escape(autodir)) 249 250 # make sure there are no files in $AUTODIR/results 251 results_path = os.path.join(autodir, 'results') 252 host.run('rm -rf %s/*' % utils.sh_escape(results_path), 253 ignore_status=True) 254 255 # Fetch the autotest client from the nearest repository 256 if use_packaging: 257 try: 258 self._install_using_packaging(host, autodir) 259 logging.info("Installation of autotest completed using the " 260 "packaging system.") 261 return 262 except (error.PackageInstallError, error.AutoservRunError, 263 global_config.ConfigError), e: 264 logging.info("Could not install autotest using the packaging " 265 "system: %s. Trying other methods", e) 266 else: 267 # Delete the package checksum file to force dut updating local 268 # packages. 269 command = ('rm -f "%s"' % 270 (os.path.join(autodir, base_packages.CHECKSUM_FILE))) 271 host.run(command) 272 273 # try to install from file or directory 274 if self.source_material: 275 c = global_config.global_config 276 supports_autoserv_packaging = c.get_config_value( 277 "PACKAGES", "serve_packages_from_autoserv", type=bool) 278 # Copy autotest recursively 279 if supports_autoserv_packaging and use_autoserv: 280 self._install_using_send_file(host, autodir) 281 else: 282 host.send_file(self.source_material, autodir, delete_dest=True) 283 logging.info("Installation of autotest completed from %s", 284 self.source_material) 285 self.installed = True 286 return 287 288 # if that fails try to install using svn 289 if utils.run('which svn').exit_status: 290 raise error.AutoservError('svn not found on target machine: %s' % 291 host.name) 292 try: 293 host.run('svn checkout %s %s' % (AUTOTEST_SVN, autodir)) 294 except error.AutoservRunError, e: 295 host.run('svn checkout %s %s' % (AUTOTEST_HTTP, autodir)) 296 logging.info("Installation of autotest completed using SVN.") 297 self.installed = True 298 299 300 def uninstall(self, host=None): 301 """ 302 Uninstall (i.e. delete) autotest. Removes the autotest client install 303 from the specified host. 304 305 @params host a Host instance from which the client will be removed 306 """ 307 if not self.installed: 308 return 309 if not host: 310 host = self.host 311 autodir = host.get_autodir() 312 if not autodir: 313 return 314 315 # perform the actual uninstall 316 host.run("rm -rf %s" % utils.sh_escape(autodir), ignore_status=True) 317 host.set_autodir(None) 318 self.installed = False 319 320 321 def get(self, location=None): 322 if not location: 323 location = os.path.join(self.serverdir, '../client') 324 location = os.path.abspath(location) 325 # If there's stuff run on our client directory already, it 326 # can cause problems. Try giving it a quick clean first. 327 cwd = os.getcwd() 328 os.chdir(location) 329 try: 330 utils.system('tools/make_clean', ignore_status=True) 331 finally: 332 os.chdir(cwd) 333 super(BaseAutotest, self).get(location) 334 self.got = True 335 336 337 def run(self, control_file, results_dir='.', host=None, timeout=None, 338 tag=None, parallel_flag=False, background=False, 339 client_disconnect_timeout=None, use_packaging=True): 340 """ 341 Run an autotest job on the remote machine. 342 343 @param control_file: An open file-like-obj of the control file. 344 @param results_dir: A str path where the results should be stored 345 on the local filesystem. 346 @param host: A Host instance on which the control file should 347 be run. 348 @param timeout: Maximum number of seconds to wait for the run or None. 349 @param tag: Tag name for the client side instance of autotest. 350 @param parallel_flag: Flag set when multiple jobs are run at the 351 same time. 352 @param background: Indicates that the client should be launched as 353 a background job; the code calling run will be responsible 354 for monitoring the client and collecting the results. 355 @param client_disconnect_timeout: Seconds to wait for the remote host 356 to come back after a reboot. Defaults to the host setting for 357 DEFAULT_REBOOT_TIMEOUT. 358 359 @raises AutotestRunError: If there is a problem executing 360 the control file. 361 """ 362 host = self._get_host_and_setup(host, use_packaging=use_packaging) 363 results_dir = os.path.abspath(results_dir) 364 365 if client_disconnect_timeout is None: 366 client_disconnect_timeout = host.DEFAULT_REBOOT_TIMEOUT 367 368 if tag: 369 results_dir = os.path.join(results_dir, tag) 370 371 atrun = _Run(host, results_dir, tag, parallel_flag, background) 372 self._do_run(control_file, results_dir, host, atrun, timeout, 373 client_disconnect_timeout, use_packaging=use_packaging) 374 375 376 def _get_host_and_setup(self, host, use_packaging=True): 377 if not host: 378 host = self.host 379 if not self.installed: 380 self.install(host, use_packaging=use_packaging) 381 382 host.wait_up(timeout=30) 383 return host 384 385 386 def _do_run(self, control_file, results_dir, host, atrun, timeout, 387 client_disconnect_timeout, use_packaging=True): 388 try: 389 atrun.verify_machine() 390 except: 391 logging.error("Verify failed on %s. Reinstalling autotest", 392 host.hostname) 393 self.install(host) 394 atrun.verify_machine() 395 debug = os.path.join(results_dir, 'debug') 396 try: 397 os.makedirs(debug) 398 except Exception: 399 pass 400 401 delete_file_list = [atrun.remote_control_file, 402 atrun.remote_control_file + '.state', 403 atrun.manual_control_file, 404 atrun.manual_control_file + '.state'] 405 cmd = ';'.join('rm -f ' + control for control in delete_file_list) 406 host.run(cmd, ignore_status=True) 407 408 tmppath = utils.get(control_file, local_copy=True) 409 410 # build up the initialization prologue for the control file 411 prologue_lines = [] 412 413 # Add the additional user arguments 414 prologue_lines.append("args = %r\n" % self.job.args) 415 416 # If the packaging system is being used, add the repository list. 417 repos = None 418 try: 419 if use_packaging: 420 repos = self.get_fetch_location() 421 prologue_lines.append('job.add_repository(%s)\n' % repos) 422 else: 423 logging.debug('use_packaging is set to False, do not add any ' 424 'repository.') 425 except global_config.ConfigError, e: 426 # If repos is defined packaging is enabled so log the error 427 if repos: 428 logging.error(e) 429 430 # on full-size installs, turn on any profilers the server is using 431 if not atrun.background: 432 running_profilers = host.job.profilers.add_log.iteritems() 433 for profiler, (args, dargs) in running_profilers: 434 call_args = [repr(profiler)] 435 call_args += [repr(arg) for arg in args] 436 call_args += ["%s=%r" % item for item in dargs.iteritems()] 437 prologue_lines.append("job.profilers.add(%s)\n" 438 % ", ".join(call_args)) 439 cfile = "".join(prologue_lines) 440 441 cfile += open(tmppath).read() 442 open(tmppath, "w").write(cfile) 443 444 # Create and copy state file to remote_control_file + '.state' 445 state_file = host.job.preprocess_client_state() 446 host.send_file(state_file, atrun.remote_control_file + '.init.state') 447 os.remove(state_file) 448 449 # Copy control_file to remote_control_file on the host 450 host.send_file(tmppath, atrun.remote_control_file) 451 if os.path.abspath(tmppath) != os.path.abspath(control_file): 452 os.remove(tmppath) 453 454 atrun.execute_control( 455 timeout=timeout, 456 client_disconnect_timeout=client_disconnect_timeout) 457 458 459 def run_timed_test(self, test_name, results_dir='.', host=None, 460 timeout=None, parallel_flag=False, background=False, 461 client_disconnect_timeout=None, *args, **dargs): 462 """ 463 Assemble a tiny little control file to just run one test, 464 and run it as an autotest client-side test 465 """ 466 if not host: 467 host = self.host 468 if not self.installed: 469 self.install(host) 470 opts = ["%s=%s" % (o[0], repr(o[1])) for o in dargs.items()] 471 cmd = ", ".join([repr(test_name)] + map(repr, args) + opts) 472 control = "job.run_test(%s)\n" % cmd 473 self.run(control, results_dir, host, timeout=timeout, 474 parallel_flag=parallel_flag, background=background, 475 client_disconnect_timeout=client_disconnect_timeout) 476 477 478 def run_test(self, test_name, results_dir='.', host=None, 479 parallel_flag=False, background=False, 480 client_disconnect_timeout=None, *args, **dargs): 481 self.run_timed_test(test_name, results_dir, host, timeout=None, 482 parallel_flag=parallel_flag, background=background, 483 client_disconnect_timeout=client_disconnect_timeout, 484 *args, **dargs) 485 486 487class _BaseRun(object): 488 """ 489 Represents a run of autotest control file. This class maintains 490 all the state necessary as an autotest control file is executed. 491 492 It is not intended to be used directly, rather control files 493 should be run using the run method in Autotest. 494 """ 495 def __init__(self, host, results_dir, tag, parallel_flag, background): 496 self.host = host 497 self.results_dir = results_dir 498 self.env = host.env 499 self.tag = tag 500 self.parallel_flag = parallel_flag 501 self.background = background 502 self.autodir = Autotest.get_installed_autodir(self.host) 503 control = os.path.join(self.autodir, 'control') 504 if tag: 505 control += '.' + tag 506 self.manual_control_file = control 507 self.remote_control_file = control + '.autoserv' 508 self.config_file = os.path.join(self.autodir, 'global_config.ini') 509 510 511 def verify_machine(self): 512 binary = os.path.join(self.autodir, 'bin/autotest') 513 try: 514 self.host.run('ls %s > /dev/null 2>&1' % binary) 515 except: 516 raise error.AutoservInstallError( 517 "Autotest does not appear to be installed") 518 519 if not self.parallel_flag: 520 tmpdir = os.path.join(self.autodir, 'tmp') 521 download = os.path.join(self.autodir, 'tests/download') 522 self.host.run('umount %s' % tmpdir, ignore_status=True) 523 self.host.run('umount %s' % download, ignore_status=True) 524 525 526 def get_base_cmd_args(self, section): 527 args = ['--verbose'] 528 if section > 0: 529 args.append('-c') 530 if self.tag: 531 args.append('-t %s' % self.tag) 532 if self.host.job.use_external_logging(): 533 args.append('-l') 534 if self.host.hostname: 535 args.append('--hostname=%s' % self.host.hostname) 536 args.append('--user=%s' % self.host.job.user) 537 538 args.append(self.remote_control_file) 539 return args 540 541 542 def get_background_cmd(self, section): 543 cmd = ['nohup', os.path.join(self.autodir, 'bin/autotest_client')] 544 cmd += self.get_base_cmd_args(section) 545 cmd += ['>/dev/null', '2>/dev/null', '&'] 546 return ' '.join(cmd) 547 548 549 def get_daemon_cmd(self, section, monitor_dir): 550 cmd = ['nohup', os.path.join(self.autodir, 'bin/autotestd'), 551 monitor_dir, '-H autoserv'] 552 cmd += self.get_base_cmd_args(section) 553 cmd += ['>/dev/null', '2>/dev/null', '&'] 554 return ' '.join(cmd) 555 556 557 def get_monitor_cmd(self, monitor_dir, stdout_read, stderr_read): 558 cmd = [os.path.join(self.autodir, 'bin', 'autotestd_monitor'), 559 monitor_dir, str(stdout_read), str(stderr_read)] 560 return ' '.join(cmd) 561 562 563 def get_client_log(self): 564 """Find what the "next" client.* prefix should be 565 566 @returns A string of the form client.INTEGER that should be prefixed 567 to all client debug log files. 568 """ 569 max_digit = -1 570 debug_dir = os.path.join(self.results_dir, 'debug') 571 client_logs = glob.glob(os.path.join(debug_dir, 'client.*.*')) 572 for log in client_logs: 573 _, number, _ = log.split('.', 2) 574 if number.isdigit(): 575 max_digit = max(max_digit, int(number)) 576 return 'client.%d' % (max_digit + 1) 577 578 579 def copy_client_config_file(self, client_log_prefix=None): 580 """ 581 Create and copy the client config file based on the server config. 582 583 @param client_log_prefix: Optional prefix to prepend to log files. 584 """ 585 client_config_file = self._create_client_config_file(client_log_prefix) 586 self.host.send_file(client_config_file, self.config_file) 587 os.remove(client_config_file) 588 589 590 def _create_client_config_file(self, client_log_prefix=None): 591 """ 592 Create a temporary file with the [CLIENT] section configuration values 593 taken from the server global_config.ini. 594 595 @param client_log_prefix: Optional prefix to prepend to log files. 596 597 @return: Path of the temporary file generated. 598 """ 599 config = global_config.global_config.get_section_values('CLIENT') 600 if client_log_prefix: 601 config.set('CLIENT', 'default_logging_name', client_log_prefix) 602 return self._create_aux_file(config.write) 603 604 605 def _create_aux_file(self, func, *args): 606 """ 607 Creates a temporary file and writes content to it according to a 608 content creation function. The file object is appended to *args, which 609 is then passed to the content creation function 610 611 @param func: Function that will be used to write content to the 612 temporary file. 613 @param *args: List of parameters that func takes. 614 @return: Path to the temporary file that was created. 615 """ 616 fd, path = tempfile.mkstemp(dir=self.host.job.tmpdir) 617 aux_file = os.fdopen(fd, "w") 618 try: 619 list_args = list(args) 620 list_args.append(aux_file) 621 func(*list_args) 622 finally: 623 aux_file.close() 624 return path 625 626 627 @staticmethod 628 def is_client_job_finished(last_line): 629 return bool(re.match(r'^END .*\t----\t----\t.*$', last_line)) 630 631 632 @staticmethod 633 def is_client_job_rebooting(last_line): 634 return bool(re.match(r'^\t*GOOD\t----\treboot\.start.*$', last_line)) 635 636 637 def _diagnose_dut(self, old_boot_id=None): 638 """ 639 Run diagnostic checks on a DUT. 640 641 1. ping: A dead host will not respond to pings. 642 2. ssh (happens with 3.): DUT hangs usually fail in authentication 643 but respond to pings. 644 3. Check if a reboot occured: A healthy but unexpected reboot leaves the 645 host running with a new boot id. 646 647 This method will always raise an exception from the AutotestFailure 648 family and should only get called when the reason for a test failing 649 is ambiguous. 650 651 @raises AutotestDeviceNotPingable: If the DUT doesn't respond to ping. 652 @raises AutotestDeviceNotSSHable: If we cannot SSH into the DUT. 653 @raises AutotestDeviceRebooted: If the boot id changed. 654 @raises AutotestAbort: If none of the above exceptions were raised. 655 Since we have no recourse we must abort at this stage. 656 """ 657 msg = 'Autotest client terminated unexpectedly: ' 658 if utils.ping(self.host.hostname, tries=1, deadline=1) != 0: 659 msg += 'DUT is no longer pingable, it may have rebooted or hung.\n' 660 raise AutotestDeviceNotPingable(msg) 661 662 if old_boot_id: 663 try: 664 new_boot_id = self.host.get_boot_id(timeout=60) 665 except Exception as e: 666 msg += ('DUT is pingable but not SSHable, it most likely' 667 ' sporadically rebooted during testing. %s\n' % str(e)) 668 raise AutotestDeviceNotSSHable(msg) 669 else: 670 if new_boot_id != old_boot_id: 671 msg += 'DUT rebooted during the test run.\n' 672 raise AutotestDeviceRebooted(msg) 673 674 msg += ('DUT is pingable, SSHable and did NOT restart ' 675 'un-expectedly. We probably lost connectivity during the ' 676 'test.') 677 else: 678 msg += ('DUT is pingable, could not determine if an un-expected ' 679 'reboot occured during the test.') 680 681 raise AutotestAbort(msg) 682 683 684 def log_unexpected_abort(self, stderr_redirector, old_boot_id=None): 685 """ 686 Logs that something unexpected happened, then tries to diagnose the 687 failure. The purpose of this function is only to close out the status 688 log with the appropriate error message, not to critically terminate 689 the program. 690 691 @param stderr_redirector: log stream. 692 @param old_boot_id: boot id used to infer if a reboot occured. 693 """ 694 stderr_redirector.flush_all_buffers() 695 try: 696 self._diagnose_dut(old_boot_id) 697 except AutotestFailure as e: 698 self.host.job.record('END ABORT', None, None, str(e)) 699 700 701 def _execute_in_background(self, section, timeout): 702 full_cmd = self.get_background_cmd(section) 703 devnull = open(os.devnull, "w") 704 705 self.copy_client_config_file(self.get_client_log()) 706 707 self.host.job.push_execution_context(self.results_dir) 708 try: 709 result = self.host.run(full_cmd, ignore_status=True, 710 timeout=timeout, 711 stdout_tee=devnull, 712 stderr_tee=devnull) 713 finally: 714 self.host.job.pop_execution_context() 715 716 return result 717 718 719 @staticmethod 720 def _strip_stderr_prologue(stderr): 721 """Strips the 'standard' prologue that get pre-pended to every 722 remote command and returns the text that was actually written to 723 stderr by the remote command.""" 724 stderr_lines = stderr.split("\n")[1:] 725 if not stderr_lines: 726 return "" 727 elif stderr_lines[0].startswith("NOTE: autotestd_monitor"): 728 del stderr_lines[0] 729 return "\n".join(stderr_lines) 730 731 732 def _execute_daemon(self, section, timeout, stderr_redirector, 733 client_disconnect_timeout): 734 monitor_dir = self.host.get_tmp_dir() 735 daemon_cmd = self.get_daemon_cmd(section, monitor_dir) 736 737 # grab the location for the server-side client log file 738 client_log_prefix = self.get_client_log() 739 client_log_path = os.path.join(self.results_dir, 'debug', 740 client_log_prefix + '.log') 741 client_log = open(client_log_path, 'w', 0) 742 self.copy_client_config_file(client_log_prefix) 743 744 stdout_read = stderr_read = 0 745 self.host.job.push_execution_context(self.results_dir) 746 try: 747 self.host.run(daemon_cmd, ignore_status=True, timeout=timeout) 748 disconnect_warnings = [] 749 while True: 750 monitor_cmd = self.get_monitor_cmd(monitor_dir, stdout_read, 751 stderr_read) 752 try: 753 result = self.host.run(monitor_cmd, ignore_status=True, 754 timeout=timeout, 755 stdout_tee=client_log, 756 stderr_tee=stderr_redirector) 757 except error.AutoservRunError, e: 758 result = e.result_obj 759 result.exit_status = None 760 disconnect_warnings.append(e.description) 761 762 stderr_redirector.log_warning( 763 "Autotest client was disconnected: %s" % e.description, 764 "NETWORK") 765 except error.AutoservSSHTimeout: 766 result = utils.CmdResult(monitor_cmd, "", "", None, 0) 767 stderr_redirector.log_warning( 768 "Attempt to connect to Autotest client timed out", 769 "NETWORK") 770 771 stdout_read += len(result.stdout) 772 stderr_read += len(self._strip_stderr_prologue(result.stderr)) 773 774 if result.exit_status is not None: 775 # TODO (crosbug.com/38224)- sbasi: Remove extra logging. 776 logging.debug('Result exit status is %d.', 777 result.exit_status) 778 return result 779 elif not self.host.wait_up(client_disconnect_timeout): 780 raise error.AutoservSSHTimeout( 781 "client was disconnected, reconnect timed out") 782 finally: 783 client_log.close() 784 self.host.job.pop_execution_context() 785 786 787 def execute_section(self, section, timeout, stderr_redirector, 788 client_disconnect_timeout): 789 logging.info("Executing %s/bin/autotest %s/control phase %d", 790 self.autodir, self.autodir, section) 791 792 if self.background: 793 result = self._execute_in_background(section, timeout) 794 else: 795 result = self._execute_daemon(section, timeout, stderr_redirector, 796 client_disconnect_timeout) 797 798 last_line = stderr_redirector.last_line 799 800 # check if we failed hard enough to warrant an exception 801 if result.exit_status == 1: 802 err = error.AutotestRunError("client job was aborted") 803 elif not self.background and not result.stderr: 804 err = error.AutotestRunError( 805 "execute_section %s failed to return anything\n" 806 "stdout:%s\n" % (section, result.stdout)) 807 else: 808 err = None 809 810 # log something if the client failed AND never finished logging 811 if err and not self.is_client_job_finished(last_line): 812 self.log_unexpected_abort(stderr_redirector) 813 814 if err: 815 raise err 816 else: 817 return stderr_redirector.last_line 818 819 820 def _wait_for_reboot(self, old_boot_id): 821 logging.info("Client is rebooting") 822 logging.info("Waiting for client to halt") 823 if not self.host.wait_down(self.host.WAIT_DOWN_REBOOT_TIMEOUT, 824 old_boot_id=old_boot_id): 825 err = "%s failed to shutdown after %d" 826 err %= (self.host.hostname, self.host.WAIT_DOWN_REBOOT_TIMEOUT) 827 raise error.AutotestRunError(err) 828 logging.info("Client down, waiting for restart") 829 if not self.host.wait_up(self.host.DEFAULT_REBOOT_TIMEOUT): 830 # since reboot failed 831 # hardreset the machine once if possible 832 # before failing this control file 833 warning = "%s did not come back up, hard resetting" 834 warning %= self.host.hostname 835 logging.warning(warning) 836 try: 837 self.host.hardreset(wait=False) 838 except (AttributeError, error.AutoservUnsupportedError): 839 warning = "Hard reset unsupported on %s" 840 warning %= self.host.hostname 841 logging.warning(warning) 842 raise error.AutotestRunError("%s failed to boot after %ds" % 843 (self.host.hostname, 844 self.host.DEFAULT_REBOOT_TIMEOUT)) 845 self.host.reboot_followup() 846 847 848 def execute_control(self, timeout=None, client_disconnect_timeout=None): 849 if not self.background: 850 collector = log_collector(self.host, self.tag, self.results_dir) 851 hostname = self.host.hostname 852 remote_results = collector.client_results_dir 853 local_results = collector.server_results_dir 854 self.host.job.add_client_log(hostname, remote_results, 855 local_results) 856 job_record_context = self.host.job.get_record_context() 857 858 section = 0 859 start_time = time.time() 860 861 logger = client_logger(self.host, self.tag, self.results_dir) 862 try: 863 while not timeout or time.time() < start_time + timeout: 864 if timeout: 865 section_timeout = start_time + timeout - time.time() 866 else: 867 section_timeout = None 868 boot_id = self.host.get_boot_id() 869 last = self.execute_section(section, section_timeout, 870 logger, client_disconnect_timeout) 871 if self.background: 872 return 873 section += 1 874 if self.is_client_job_finished(last): 875 logging.info("Client complete") 876 return 877 elif self.is_client_job_rebooting(last): 878 try: 879 self._wait_for_reboot(boot_id) 880 except error.AutotestRunError, e: 881 self.host.job.record("ABORT", None, "reboot", str(e)) 882 self.host.job.record("END ABORT", None, None, str(e)) 883 raise 884 continue 885 886 # If a test fails without probable cause we try to bucket it's 887 # failure into one of 2 categories. If we can determine the 888 # current state of the device and it is suspicious, we close the 889 # status lines indicating a failure. If we either cannot 890 # determine the state of the device, or it appears totally 891 # healthy, we give up and abort. 892 try: 893 self._diagnose_dut(boot_id) 894 except AutotestDeviceError as e: 895 # The status lines of the test are pretty much tailed to 896 # our log, with indentation, from the client job on the DUT. 897 # So if the DUT goes down unexpectedly we'll end up with a 898 # malformed status log unless we manually unwind the status 899 # stack. Ideally we would want to write a nice wrapper like 900 # server_job methods run_reboot, run_group but they expect 901 # reboots and we don't. 902 self.host.job.record('FAIL', None, None, str(e)) 903 self.host.job.record('END FAIL', None, None) 904 self.host.job.record('END GOOD', None, None) 905 self.host.job.failed_with_device_error = True 906 return 907 except AutotestAbort as e: 908 self.host.job.record('ABORT', None, None, str(e)) 909 self.host.job.record('END ABORT', None, None) 910 911 # give the client machine a chance to recover from a crash 912 self.host.wait_up( 913 self.host.HOURS_TO_WAIT_FOR_RECOVERY * 3600) 914 msg = ("Aborting - unexpected final status message from " 915 "client on %s: %s\n") % (self.host.hostname, last) 916 raise error.AutotestRunError(msg) 917 finally: 918 logger.close() 919 if not self.background: 920 collector.collect_client_job_results() 921 collector.remove_redundant_client_logs() 922 state_file = os.path.basename(self.remote_control_file 923 + '.state') 924 state_path = os.path.join(self.results_dir, state_file) 925 self.host.job.postprocess_client_state(state_path) 926 self.host.job.remove_client_log(hostname, remote_results, 927 local_results) 928 job_record_context.restore() 929 930 # should only get here if we timed out 931 assert timeout 932 raise error.AutotestTimeoutError() 933 934 935class log_collector(object): 936 def __init__(self, host, client_tag, results_dir): 937 self.host = host 938 if not client_tag: 939 client_tag = "default" 940 self.client_results_dir = os.path.join(host.get_autodir(), "results", 941 client_tag) 942 self.server_results_dir = results_dir 943 944 945 def collect_client_job_results(self): 946 """ A method that collects all the current results of a running 947 client job into the results dir. By default does nothing as no 948 client job is running, but when running a client job you can override 949 this with something that will actually do something. """ 950 951 # make an effort to wait for the machine to come up 952 try: 953 self.host.wait_up(timeout=30) 954 except error.AutoservError: 955 # don't worry about any errors, we'll try and 956 # get the results anyway 957 pass 958 959 # Copy all dirs in default to results_dir 960 timer = autotest_stats.Timer('collect_client_job_results') 961 timer.start() 962 try: 963 self.host.get_file(self.client_results_dir + '/', 964 self.server_results_dir, preserve_symlinks=True) 965 966 # Only report time used for successful get_file calls. 967 timer.stop(); 968 except Exception: 969 # well, don't stop running just because we couldn't get logs 970 e_msg = "Unexpected error copying test result logs, continuing ..." 971 logging.error(e_msg) 972 traceback.print_exc(file=sys.stdout) 973 974 975 def remove_redundant_client_logs(self): 976 """Remove client.*.log files in favour of client.*.DEBUG files.""" 977 debug_dir = os.path.join(self.server_results_dir, 'debug') 978 debug_files = [f for f in os.listdir(debug_dir) 979 if re.search(r'^client\.\d+\.DEBUG$', f)] 980 for debug_file in debug_files: 981 log_file = debug_file.replace('DEBUG', 'log') 982 log_file = os.path.join(debug_dir, log_file) 983 if os.path.exists(log_file): 984 os.remove(log_file) 985 986 987# a file-like object for catching stderr from an autotest client and 988# extracting status logs from it 989class BaseClientLogger(object): 990 """Partial file object to write to both stdout and 991 the status log file. We only implement those methods 992 utils.run() actually calls. 993 """ 994 status_parser = re.compile(r"^AUTOTEST_STATUS:([^:]*):(.*)$") 995 test_complete_parser = re.compile(r"^AUTOTEST_TEST_COMPLETE:(.*)$") 996 fetch_package_parser = re.compile( 997 r"^AUTOTEST_FETCH_PACKAGE:([^:]*):([^:]*):(.*)$") 998 extract_indent = re.compile(r"^(\t*).*$") 999 extract_timestamp = re.compile(r".*\ttimestamp=(\d+)\t.*$") 1000 1001 def __init__(self, host, tag, server_results_dir): 1002 self.host = host 1003 self.job = host.job 1004 self.log_collector = log_collector(host, tag, server_results_dir) 1005 self.leftover = "" 1006 self.last_line = "" 1007 self.logs = {} 1008 1009 1010 def _process_log_dict(self, log_dict): 1011 log_list = log_dict.pop("logs", []) 1012 for key in sorted(log_dict.iterkeys()): 1013 log_list += self._process_log_dict(log_dict.pop(key)) 1014 return log_list 1015 1016 1017 def _process_logs(self): 1018 """Go through the accumulated logs in self.log and print them 1019 out to stdout and the status log. Note that this processes 1020 logs in an ordering where: 1021 1022 1) logs to different tags are never interleaved 1023 2) logs to x.y come before logs to x.y.z for all z 1024 3) logs to x.y come before x.z whenever y < z 1025 1026 Note that this will in general not be the same as the 1027 chronological ordering of the logs. However, if a chronological 1028 ordering is desired that one can be reconstructed from the 1029 status log by looking at timestamp lines.""" 1030 log_list = self._process_log_dict(self.logs) 1031 for entry in log_list: 1032 self.job.record_entry(entry, log_in_subdir=False) 1033 if log_list: 1034 self.last_line = log_list[-1].render() 1035 1036 1037 def _process_quoted_line(self, tag, line): 1038 """Process a line quoted with an AUTOTEST_STATUS flag. If the 1039 tag is blank then we want to push out all the data we've been 1040 building up in self.logs, and then the newest line. If the 1041 tag is not blank, then push the line into the logs for handling 1042 later.""" 1043 entry = base_job.status_log_entry.parse(line) 1044 if entry is None: 1045 return # the line contains no status lines 1046 if tag == "": 1047 self._process_logs() 1048 self.job.record_entry(entry, log_in_subdir=False) 1049 self.last_line = line 1050 else: 1051 tag_parts = [int(x) for x in tag.split(".")] 1052 log_dict = self.logs 1053 for part in tag_parts: 1054 log_dict = log_dict.setdefault(part, {}) 1055 log_list = log_dict.setdefault("logs", []) 1056 log_list.append(entry) 1057 1058 1059 def _process_info_line(self, line): 1060 """Check if line is an INFO line, and if it is, interpret any control 1061 messages (e.g. enabling/disabling warnings) that it may contain.""" 1062 match = re.search(r"^\t*INFO\t----\t----(.*)\t[^\t]*$", line) 1063 if not match: 1064 return # not an INFO line 1065 for field in match.group(1).split('\t'): 1066 if field.startswith("warnings.enable="): 1067 func = self.job.warning_manager.enable_warnings 1068 elif field.startswith("warnings.disable="): 1069 func = self.job.warning_manager.disable_warnings 1070 else: 1071 continue 1072 warning_type = field.split("=", 1)[1] 1073 func(warning_type) 1074 1075 1076 def _process_line(self, line): 1077 """Write out a line of data to the appropriate stream. Status 1078 lines sent by autotest will be prepended with 1079 "AUTOTEST_STATUS", and all other lines are ssh error 1080 messages.""" 1081 status_match = self.status_parser.search(line) 1082 test_complete_match = self.test_complete_parser.search(line) 1083 fetch_package_match = self.fetch_package_parser.search(line) 1084 if status_match: 1085 tag, line = status_match.groups() 1086 self._process_info_line(line) 1087 self._process_quoted_line(tag, line) 1088 elif test_complete_match: 1089 self._process_logs() 1090 fifo_path, = test_complete_match.groups() 1091 try: 1092 self.log_collector.collect_client_job_results() 1093 self.host.run("echo A > %s" % fifo_path) 1094 except Exception: 1095 msg = "Post-test log collection failed, continuing anyway" 1096 logging.exception(msg) 1097 elif fetch_package_match: 1098 pkg_name, dest_path, fifo_path = fetch_package_match.groups() 1099 serve_packages = global_config.global_config.get_config_value( 1100 "PACKAGES", "serve_packages_from_autoserv", type=bool) 1101 if serve_packages and pkg_name.endswith(".tar.bz2"): 1102 try: 1103 self._send_tarball(pkg_name, dest_path) 1104 except Exception: 1105 msg = "Package tarball creation failed, continuing anyway" 1106 logging.exception(msg) 1107 try: 1108 self.host.run("echo B > %s" % fifo_path) 1109 except Exception: 1110 msg = "Package tarball installation failed, continuing anyway" 1111 logging.exception(msg) 1112 else: 1113 logging.info(line) 1114 1115 1116 def _send_tarball(self, pkg_name, remote_dest): 1117 name, pkg_type = self.job.pkgmgr.parse_tarball_name(pkg_name) 1118 src_dirs = [] 1119 if pkg_type == 'test': 1120 for test_dir in ['site_tests', 'tests']: 1121 src_dir = os.path.join(self.job.clientdir, test_dir, name) 1122 if os.path.exists(src_dir): 1123 src_dirs += [src_dir] 1124 if autoserv_prebuild: 1125 prebuild.setup(self.job.clientdir, src_dir) 1126 break 1127 elif pkg_type == 'profiler': 1128 src_dirs += [os.path.join(self.job.clientdir, 'profilers', name)] 1129 if autoserv_prebuild: 1130 prebuild.setup(self.job.clientdir, src_dir) 1131 elif pkg_type == 'dep': 1132 src_dirs += [os.path.join(self.job.clientdir, 'deps', name)] 1133 elif pkg_type == 'client': 1134 return # you must already have a client to hit this anyway 1135 else: 1136 return # no other types are supported 1137 1138 # iterate over src_dirs until we find one that exists, then tar it 1139 for src_dir in src_dirs: 1140 if os.path.exists(src_dir): 1141 try: 1142 logging.info('Bundling %s into %s', src_dir, pkg_name) 1143 temp_dir = autotemp.tempdir(unique_id='autoserv-packager', 1144 dir=self.job.tmpdir) 1145 tarball_path = self.job.pkgmgr.tar_package( 1146 pkg_name, src_dir, temp_dir.name, " .") 1147 self.host.send_file(tarball_path, remote_dest) 1148 finally: 1149 temp_dir.clean() 1150 return 1151 1152 1153 def log_warning(self, msg, warning_type): 1154 """Injects a WARN message into the current status logging stream.""" 1155 timestamp = int(time.time()) 1156 if self.job.warning_manager.is_valid(timestamp, warning_type): 1157 self.job.record('WARN', None, None, msg) 1158 1159 1160 def write(self, data): 1161 # now start processing the existing buffer and the new data 1162 data = self.leftover + data 1163 lines = data.split('\n') 1164 processed_lines = 0 1165 try: 1166 # process all the buffered data except the last line 1167 # ignore the last line since we may not have all of it yet 1168 for line in lines[:-1]: 1169 self._process_line(line) 1170 processed_lines += 1 1171 finally: 1172 # save any unprocessed lines for future processing 1173 self.leftover = '\n'.join(lines[processed_lines:]) 1174 1175 1176 def flush(self): 1177 sys.stdout.flush() 1178 1179 1180 def flush_all_buffers(self): 1181 if self.leftover: 1182 self._process_line(self.leftover) 1183 self.leftover = "" 1184 self._process_logs() 1185 self.flush() 1186 1187 1188 def close(self): 1189 self.flush_all_buffers() 1190 1191 1192SiteAutotest = client_utils.import_site_class( 1193 __file__, "autotest_lib.server.site_autotest", "SiteAutotest", 1194 BaseAutotest) 1195 1196 1197_SiteRun = client_utils.import_site_class( 1198 __file__, "autotest_lib.server.site_autotest", "_SiteRun", _BaseRun) 1199 1200 1201SiteClientLogger = client_utils.import_site_class( 1202 __file__, "autotest_lib.server.site_autotest", "SiteClientLogger", 1203 BaseClientLogger) 1204 1205 1206class Autotest(SiteAutotest): 1207 pass 1208 1209 1210class client_logger(SiteClientLogger): 1211 pass 1212 1213 1214class _Run(_SiteRun): 1215 pass 1216