abstract_ssh.py revision 04be2bd5e4666a5c253e9c30ab20555e04286032
1import os, time, socket, shutil, glob, logging, traceback, tempfile 2from autotest_lib.client.common_lib import autotemp, error 3from autotest_lib.server import utils, autotest 4from autotest_lib.server.hosts import remote 5from autotest_lib.client.common_lib.global_config import global_config 6 7# pylint: disable-msg=C0111 8 9get_value = global_config.get_config_value 10enable_master_ssh = get_value('AUTOSERV', 'enable_master_ssh', type=bool, 11 default=False) 12 13 14class AbstractSSHHost(remote.RemoteHost): 15 """ 16 This class represents a generic implementation of most of the 17 framework necessary for controlling a host via ssh. It implements 18 almost all of the abstract Host methods, except for the core 19 Host.run method. 20 """ 21 22 def _initialize(self, hostname, user="root", port=22, password="", 23 *args, **dargs): 24 super(AbstractSSHHost, self)._initialize(hostname=hostname, 25 *args, **dargs) 26 self.ip = socket.getaddrinfo(self.hostname, None)[0][4][0] 27 self.user = user 28 self.port = port 29 self.password = password 30 self._use_rsync = None 31 self.known_hosts_file = tempfile.mkstemp()[1] 32 33 """ 34 Master SSH connection background job, socket temp directory and socket 35 control path option. If master-SSH is enabled, these fields will be 36 initialized by start_master_ssh when a new SSH connection is initiated. 37 """ 38 self.master_ssh_job = None 39 self.master_ssh_tempdir = None 40 self.master_ssh_option = '' 41 42 43 def make_ssh_command(self, user="root", port=22, opts='', 44 hosts_file='/dev/null', 45 connect_timeout=30, alive_interval=300): 46 base_command = ("/usr/bin/ssh -a -x %s -o StrictHostKeyChecking=no " 47 "-o UserKnownHostsFile=%s -o BatchMode=yes " 48 "-o ConnectTimeout=%d -o ServerAliveInterval=%d " 49 "-l %s -p %d") 50 assert isinstance(connect_timeout, (int, long)) 51 assert connect_timeout > 0 # can't disable the timeout 52 return base_command % (opts, hosts_file, connect_timeout, 53 alive_interval, user, port) 54 55 56 def use_rsync(self): 57 if self._use_rsync is not None: 58 return self._use_rsync 59 60 # Check if rsync is available on the remote host. If it's not, 61 # don't try to use it for any future file transfers. 62 self._use_rsync = self._check_rsync() 63 if not self._use_rsync: 64 logging.warning("rsync not available on remote host %s -- disabled", 65 self.hostname) 66 return self._use_rsync 67 68 69 def _check_rsync(self): 70 """ 71 Check if rsync is available on the remote host. 72 """ 73 try: 74 self.run("rsync --version", stdout_tee=None, stderr_tee=None) 75 except error.AutoservRunError: 76 return False 77 return True 78 79 80 def _encode_remote_paths(self, paths, escape=True): 81 """ 82 Given a list of file paths, encodes it as a single remote path, in 83 the style used by rsync and scp. 84 """ 85 if escape: 86 paths = [utils.scp_remote_escape(path) for path in paths] 87 return '%s@%s:"%s"' % (self.user, self.hostname, " ".join(paths)) 88 89 90 def _make_rsync_cmd(self, sources, dest, delete_dest, preserve_symlinks): 91 """ 92 Given a list of source paths and a destination path, produces the 93 appropriate rsync command for copying them. Remote paths must be 94 pre-encoded. 95 """ 96 ssh_cmd = self.make_ssh_command(user=self.user, port=self.port, 97 opts=self.master_ssh_option, 98 hosts_file=self.known_hosts_file) 99 if delete_dest: 100 delete_flag = "--delete" 101 else: 102 delete_flag = "" 103 if preserve_symlinks: 104 symlink_flag = "" 105 else: 106 symlink_flag = "-L" 107 command = ("rsync %s %s --timeout=1800 --rsh='%s' -az --no-o --no-g " 108 "%s %s") 109 return command % (symlink_flag, delete_flag, ssh_cmd, 110 " ".join(sources), dest) 111 112 113 def _make_ssh_cmd(self, cmd): 114 """ 115 Create a base ssh command string for the host which can be used 116 to run commands directly on the machine 117 """ 118 base_cmd = self.make_ssh_command(user=self.user, port=self.port, 119 opts=self.master_ssh_option, 120 hosts_file=self.known_hosts_file) 121 122 return '%s %s "%s"' % (base_cmd, self.hostname, utils.sh_escape(cmd)) 123 124 def _make_scp_cmd(self, sources, dest): 125 """ 126 Given a list of source paths and a destination path, produces the 127 appropriate scp command for encoding it. Remote paths must be 128 pre-encoded. 129 """ 130 command = ("scp -rq %s -o StrictHostKeyChecking=no " 131 "-o UserKnownHostsFile=%s -P %d %s '%s'") 132 return command % (self.master_ssh_option, self.known_hosts_file, 133 self.port, " ".join(sources), dest) 134 135 136 def _make_rsync_compatible_globs(self, path, is_local): 137 """ 138 Given an rsync-style path, returns a list of globbed paths 139 that will hopefully provide equivalent behaviour for scp. Does not 140 support the full range of rsync pattern matching behaviour, only that 141 exposed in the get/send_file interface (trailing slashes). 142 143 The is_local param is flag indicating if the paths should be 144 interpreted as local or remote paths. 145 """ 146 147 # non-trailing slash paths should just work 148 if len(path) == 0 or path[-1] != "/": 149 return [path] 150 151 # make a function to test if a pattern matches any files 152 if is_local: 153 def glob_matches_files(path, pattern): 154 return len(glob.glob(path + pattern)) > 0 155 else: 156 def glob_matches_files(path, pattern): 157 result = self.run("ls \"%s\"%s" % (utils.sh_escape(path), 158 pattern), 159 stdout_tee=None, ignore_status=True) 160 return result.exit_status == 0 161 162 # take a set of globs that cover all files, and see which are needed 163 patterns = ["*", ".[!.]*"] 164 patterns = [p for p in patterns if glob_matches_files(path, p)] 165 166 # convert them into a set of paths suitable for the commandline 167 if is_local: 168 return ["\"%s\"%s" % (utils.sh_escape(path), pattern) 169 for pattern in patterns] 170 else: 171 return [utils.scp_remote_escape(path) + pattern 172 for pattern in patterns] 173 174 175 def _make_rsync_compatible_source(self, source, is_local): 176 """ 177 Applies the same logic as _make_rsync_compatible_globs, but 178 applies it to an entire list of sources, producing a new list of 179 sources, properly quoted. 180 """ 181 return sum((self._make_rsync_compatible_globs(path, is_local) 182 for path in source), []) 183 184 185 def _set_umask_perms(self, dest): 186 """ 187 Given a destination file/dir (recursively) set the permissions on 188 all the files and directories to the max allowed by running umask. 189 """ 190 191 # now this looks strange but I haven't found a way in Python to _just_ 192 # get the umask, apparently the only option is to try to set it 193 umask = os.umask(0) 194 os.umask(umask) 195 196 max_privs = 0777 & ~umask 197 198 def set_file_privs(filename): 199 """Sets mode of |filename|. Assumes |filename| exists.""" 200 file_stat = os.stat(filename) 201 202 file_privs = max_privs 203 # if the original file permissions do not have at least one 204 # executable bit then do not set it anywhere 205 if not file_stat.st_mode & 0111: 206 file_privs &= ~0111 207 208 os.chmod(filename, file_privs) 209 210 # try a bottom-up walk so changes on directory permissions won't cut 211 # our access to the files/directories inside it 212 for root, dirs, files in os.walk(dest, topdown=False): 213 # when setting the privileges we emulate the chmod "X" behaviour 214 # that sets to execute only if it is a directory or any of the 215 # owner/group/other already has execute right 216 for dirname in dirs: 217 os.chmod(os.path.join(root, dirname), max_privs) 218 219 # Filter out broken symlinks as we go. 220 for filename in filter(os.path.exists, files): 221 set_file_privs(os.path.join(root, filename)) 222 223 224 # now set privs for the dest itself 225 if os.path.isdir(dest): 226 os.chmod(dest, max_privs) 227 else: 228 set_file_privs(dest) 229 230 231 def get_file(self, source, dest, delete_dest=False, preserve_perm=True, 232 preserve_symlinks=False): 233 """ 234 Copy files from the remote host to a local path. 235 236 Directories will be copied recursively. 237 If a source component is a directory with a trailing slash, 238 the content of the directory will be copied, otherwise, the 239 directory itself and its content will be copied. This 240 behavior is similar to that of the program 'rsync'. 241 242 Args: 243 source: either 244 1) a single file or directory, as a string 245 2) a list of one or more (possibly mixed) 246 files or directories 247 dest: a file or a directory (if source contains a 248 directory or more than one element, you must 249 supply a directory dest) 250 delete_dest: if this is true, the command will also clear 251 out any old files at dest that are not in the 252 source 253 preserve_perm: tells get_file() to try to preserve the sources 254 permissions on files and dirs 255 preserve_symlinks: try to preserve symlinks instead of 256 transforming them into files/dirs on copy 257 258 Raises: 259 AutoservRunError: the scp command failed 260 """ 261 logging.debug('get_file. source: %s, dest: %s, delete_dest: %s,' 262 'preserve_perm: %s, preserve_symlinks:%s', source, dest, 263 delete_dest, preserve_perm, preserve_symlinks) 264 # Start a master SSH connection if necessary. 265 self.start_master_ssh() 266 267 if isinstance(source, basestring): 268 source = [source] 269 dest = os.path.abspath(dest) 270 271 # If rsync is disabled or fails, try scp. 272 try_scp = True 273 if self.use_rsync(): 274 logging.debug('Using Rsync.') 275 try: 276 remote_source = self._encode_remote_paths(source) 277 local_dest = utils.sh_escape(dest) 278 rsync = self._make_rsync_cmd([remote_source], local_dest, 279 delete_dest, preserve_symlinks) 280 utils.run(rsync) 281 try_scp = False 282 except error.CmdError, e: 283 logging.warning("trying scp, rsync failed: %s", e) 284 285 if try_scp: 286 logging.debug('Trying scp.') 287 # scp has no equivalent to --delete, just drop the entire dest dir 288 if delete_dest and os.path.isdir(dest): 289 shutil.rmtree(dest) 290 os.mkdir(dest) 291 292 remote_source = self._make_rsync_compatible_source(source, False) 293 if remote_source: 294 # _make_rsync_compatible_source() already did the escaping 295 remote_source = self._encode_remote_paths(remote_source, 296 escape=False) 297 local_dest = utils.sh_escape(dest) 298 scp = self._make_scp_cmd([remote_source], local_dest) 299 try: 300 utils.run(scp) 301 except error.CmdError, e: 302 logging.debug('scp failed: %s', e) 303 raise error.AutoservRunError(e.args[0], e.args[1]) 304 305 if not preserve_perm: 306 # we have no way to tell scp to not try to preserve the 307 # permissions so set them after copy instead. 308 # for rsync we could use "--no-p --chmod=ugo=rwX" but those 309 # options are only in very recent rsync versions 310 self._set_umask_perms(dest) 311 312 313 def send_file(self, source, dest, delete_dest=False, 314 preserve_symlinks=False): 315 """ 316 Copy files from a local path to the remote host. 317 318 Directories will be copied recursively. 319 If a source component is a directory with a trailing slash, 320 the content of the directory will be copied, otherwise, the 321 directory itself and its content will be copied. This 322 behavior is similar to that of the program 'rsync'. 323 324 Args: 325 source: either 326 1) a single file or directory, as a string 327 2) a list of one or more (possibly mixed) 328 files or directories 329 dest: a file or a directory (if source contains a 330 directory or more than one element, you must 331 supply a directory dest) 332 delete_dest: if this is true, the command will also clear 333 out any old files at dest that are not in the 334 source 335 preserve_symlinks: controls if symlinks on the source will be 336 copied as such on the destination or transformed into the 337 referenced file/directory 338 339 Raises: 340 AutoservRunError: the scp command failed 341 """ 342 logging.debug('send_file. source: %s, dest: %s, delete_dest: %s,' 343 'preserve_symlinks:%s', source, dest, 344 delete_dest, preserve_symlinks) 345 # Start a master SSH connection if necessary. 346 self.start_master_ssh() 347 348 if isinstance(source, basestring): 349 source = [source] 350 remote_dest = self._encode_remote_paths([dest]) 351 352 # If rsync is disabled or fails, try scp. 353 try_scp = True 354 if self.use_rsync(): 355 logging.debug('Using Rsync.') 356 try: 357 local_sources = [utils.sh_escape(path) for path in source] 358 rsync = self._make_rsync_cmd(local_sources, remote_dest, 359 delete_dest, preserve_symlinks) 360 utils.run(rsync) 361 try_scp = False 362 except error.CmdError, e: 363 logging.warning("trying scp, rsync failed: %s", e) 364 365 if try_scp: 366 logging.debug('Trying scp.') 367 # scp has no equivalent to --delete, just drop the entire dest dir 368 if delete_dest: 369 is_dir = self.run("ls -d %s/" % dest, 370 ignore_status=True).exit_status == 0 371 if is_dir: 372 cmd = "rm -rf %s && mkdir %s" 373 cmd %= (dest, dest) 374 self.run(cmd) 375 376 local_sources = self._make_rsync_compatible_source(source, True) 377 if local_sources: 378 scp = self._make_scp_cmd(local_sources, remote_dest) 379 try: 380 utils.run(scp) 381 except error.CmdError, e: 382 logging.debug('scp failed: %s', e) 383 raise error.AutoservRunError(e.args[0], e.args[1]) 384 385 386 def ssh_ping(self, timeout=60, base_cmd='true'): 387 """ 388 Pings remote host via ssh. 389 390 @param timeout: Time in seconds before giving up. 391 Defaults to 60 seconds. 392 @param base_cmd: The base command to run with the ssh ping. 393 Defaults to true. 394 @raise AutoservSSHTimeout: If the ssh ping times out. 395 @raise AutoservSshPermissionDeniedError: If ssh ping fails due to 396 permissions. 397 @raise AutoservSshPingHostError: For other AutoservRunErrors. 398 """ 399 try: 400 self.run(base_cmd, timeout=timeout, connect_timeout=timeout) 401 except error.AutoservSSHTimeout: 402 msg = "Host (ssh) verify timed out (timeout = %d)" % timeout 403 raise error.AutoservSSHTimeout(msg) 404 except error.AutoservSshPermissionDeniedError: 405 #let AutoservSshPermissionDeniedError be visible to the callers 406 raise 407 except error.AutoservRunError, e: 408 # convert the generic AutoservRunError into something more 409 # specific for this context 410 raise error.AutoservSshPingHostError(e.description + '\n' + 411 repr(e.result_obj)) 412 413 414 def is_up(self, timeout=60, base_cmd='true'): 415 """ 416 Check if the remote host is up by ssh-ing and running a base command. 417 418 @param timeout: timeout in seconds. 419 @param base_cmd: a base command to run with ssh. The default is 'true'. 420 @returns True if the remote host is up before the timeout expires, 421 False otherwise. 422 """ 423 try: 424 self.ssh_ping(timeout=timeout, base_cmd=base_cmd) 425 except error.AutoservError: 426 return False 427 else: 428 return True 429 430 431 def wait_up(self, timeout=None): 432 """ 433 Wait until the remote host is up or the timeout expires. 434 435 In fact, it will wait until an ssh connection to the remote 436 host can be established, and getty is running. 437 438 @param timeout time limit in seconds before returning even 439 if the host is not up. 440 441 @returns True if the host was found to be up before the timeout expires, 442 False otherwise 443 """ 444 if timeout: 445 current_time = int(time.time()) 446 end_time = current_time + timeout 447 448 while not timeout or current_time < end_time: 449 if self.is_up(timeout=end_time - current_time): 450 try: 451 if self.are_wait_up_processes_up(): 452 logging.debug('Host %s is now up', self.hostname) 453 return True 454 except error.AutoservError: 455 pass 456 time.sleep(1) 457 current_time = int(time.time()) 458 459 logging.debug('Host %s is still down after waiting %d seconds', 460 self.hostname, int(timeout + time.time() - end_time)) 461 return False 462 463 464 def wait_down(self, timeout=None, warning_timer=None, old_boot_id=None): 465 """ 466 Wait until the remote host is down or the timeout expires. 467 468 If old_boot_id is provided, this will wait until either the machine 469 is unpingable or self.get_boot_id() returns a value different from 470 old_boot_id. If the boot_id value has changed then the function 471 returns true under the assumption that the machine has shut down 472 and has now already come back up. 473 474 If old_boot_id is None then until the machine becomes unreachable the 475 method assumes the machine has not yet shut down. 476 477 Based on this definition, the 4 possible permutations of timeout 478 and old_boot_id are: 479 1. timeout and old_boot_id: wait timeout seconds for either the 480 host to become unpingable, or the boot id 481 to change. In the latter case we've rebooted 482 and in the former case we've only shutdown, 483 but both cases return True. 484 2. only timeout: wait timeout seconds for the host to become unpingable. 485 If the host remains pingable throughout timeout seconds 486 we return False. 487 3. only old_boot_id: wait forever until either the host becomes 488 unpingable or the boot_id changes. Return true 489 when either of those conditions are met. 490 4. not timeout, not old_boot_id: wait forever till the host becomes 491 unpingable. 492 493 @param timeout Time limit in seconds before returning even 494 if the host is still up. 495 @param warning_timer Time limit in seconds that will generate 496 a warning if the host is not down yet. 497 @param old_boot_id A string containing the result of self.get_boot_id() 498 prior to the host being told to shut down. Can be None if this is 499 not available. 500 501 @returns True if the host was found to be down, False otherwise 502 """ 503 #TODO: there is currently no way to distinguish between knowing 504 #TODO: boot_id was unsupported and not knowing the boot_id. 505 current_time = int(time.time()) 506 if timeout: 507 end_time = current_time + timeout 508 509 if warning_timer: 510 warn_time = current_time + warning_timer 511 512 if old_boot_id is not None: 513 logging.debug('Host %s pre-shutdown boot_id is %s', 514 self.hostname, old_boot_id) 515 516 # Impose semi real-time deadline constraints, since some clients 517 # (eg: watchdog timer tests) expect strict checking of time elapsed. 518 # Each iteration of this loop is treated as though it atomically 519 # completes within current_time, this is needed because if we used 520 # inline time.time() calls instead then the following could happen: 521 # 522 # while not timeout or time.time() < end_time: [23 < 30] 523 # some code. [takes 10 secs] 524 # try: 525 # new_boot_id = self.get_boot_id(timeout=end_time - time.time()) 526 # [30 - 33] 527 # The last step will lead to a return True, when in fact the machine 528 # went down at 32 seconds (>30). Hence we need to pass get_boot_id 529 # the same time that allowed us into that iteration of the loop. 530 while not timeout or current_time < end_time: 531 try: 532 new_boot_id = self.get_boot_id(timeout=end_time-current_time) 533 except error.AutoservError: 534 logging.debug('Host %s is now unreachable over ssh, is down', 535 self.hostname) 536 return True 537 else: 538 # if the machine is up but the boot_id value has changed from 539 # old boot id, then we can assume the machine has gone down 540 # and then already come back up 541 if old_boot_id is not None and old_boot_id != new_boot_id: 542 logging.debug('Host %s now has boot_id %s and so must ' 543 'have rebooted', self.hostname, new_boot_id) 544 return True 545 546 if warning_timer and current_time > warn_time: 547 self.record("INFO", None, "shutdown", 548 "Shutdown took longer than %ds" % warning_timer) 549 # Print the warning only once. 550 warning_timer = None 551 # If a machine is stuck switching runlevels 552 # This may cause the machine to reboot. 553 self.run('kill -HUP 1', ignore_status=True) 554 555 time.sleep(1) 556 current_time = int(time.time()) 557 558 return False 559 560 561 # tunable constants for the verify & repair code 562 AUTOTEST_GB_DISKSPACE_REQUIRED = get_value("SERVER", 563 "gb_diskspace_required", 564 type=float, 565 default=20.0) 566 567 568 def verify_connectivity(self): 569 super(AbstractSSHHost, self).verify_connectivity() 570 571 logging.info('Pinging host ' + self.hostname) 572 self.ssh_ping() 573 logging.info("Host (ssh) %s is alive", self.hostname) 574 575 if self.is_shutting_down(): 576 raise error.AutoservHostIsShuttingDownError("Host is shutting down") 577 578 579 def verify_software(self): 580 super(AbstractSSHHost, self).verify_software() 581 try: 582 self.check_diskspace(autotest.Autotest.get_install_dir(self), 583 self.AUTOTEST_GB_DISKSPACE_REQUIRED) 584 except error.AutoservHostError: 585 raise # only want to raise if it's a space issue 586 except autotest.AutodirNotFoundError: 587 # autotest dir may not exist, etc. ignore 588 logging.debug('autodir space check exception, this is probably ' 589 'safe to ignore\n' + traceback.format_exc()) 590 591 592 def close(self): 593 super(AbstractSSHHost, self).close() 594 self._cleanup_master_ssh() 595 os.remove(self.known_hosts_file) 596 597 598 def _cleanup_master_ssh(self): 599 """ 600 Release all resources (process, temporary directory) used by an active 601 master SSH connection. 602 """ 603 # If a master SSH connection is running, kill it. 604 if self.master_ssh_job is not None: 605 logging.debug('Nuking master_ssh_job.') 606 utils.nuke_subprocess(self.master_ssh_job.sp) 607 self.master_ssh_job = None 608 609 # Remove the temporary directory for the master SSH socket. 610 if self.master_ssh_tempdir is not None: 611 logging.debug('Cleaning master_ssh_tempdir.') 612 self.master_ssh_tempdir.clean() 613 self.master_ssh_tempdir = None 614 self.master_ssh_option = '' 615 616 617 def start_master_ssh(self, timeout=5): 618 """ 619 Called whenever a slave SSH connection needs to be initiated (e.g., by 620 run, rsync, scp). If master SSH support is enabled and a master SSH 621 connection is not active already, start a new one in the background. 622 Also, cleanup any zombie master SSH connections (e.g., dead due to 623 reboot). 624 625 timeout: timeout in seconds (default 5) to wait for master ssh 626 connection to be established. If timeout is reached, a 627 warning message is logged, but no other action is taken. 628 """ 629 if not enable_master_ssh: 630 return 631 632 # If a previously started master SSH connection is not running 633 # anymore, it needs to be cleaned up and then restarted. 634 if self.master_ssh_job is not None: 635 socket_path = os.path.join(self.master_ssh_tempdir.name, 'socket') 636 if (not os.path.exists(socket_path) or 637 self.master_ssh_job.sp.poll() is not None): 638 if self.master_ssh_job.sp.poll() is None: 639 logging.warning('Master ssh connection socket file ' 640 'was missing while its subprocess was ' 641 'still running.') 642 if os.path.exists(self.master_ssh_tempdir.name): 643 logging.warning('However, the socket file temporary ' 644 'directory still exists.') 645 646 logging.warning('Info on defunct master ssh ps below.') 647 master_pid = str(self.master_ssh_job.sp.pid) 648 ps_output = utils.run(['ps', '-Fww', master_pid], 649 ignore_status=True).stdout 650 logging.warning('Master ssh connection ps info: %s', 651 ps_output) 652 lsof_output = utils.run(['lsof', '-p', master_pid], 653 ignore_status=True).stdout 654 logging.warning('Master ssh connection lsof info: %s', 655 lsof_output) 656 657 logging.info("Master ssh connection to %s is down.", 658 self.hostname) 659 self._cleanup_master_ssh() 660 661 # Start a new master SSH connection. 662 if self.master_ssh_job is None: 663 # Create a shared socket in a temp location. 664 self.master_ssh_tempdir = autotemp.tempdir(unique_id='ssh-master') 665 self.master_ssh_option = ("-o ControlPath=%s/socket" % 666 self.master_ssh_tempdir.name) 667 668 # Start the master SSH connection in the background. 669 master_cmd = self.ssh_command(options="-N -o ControlMaster=yes") 670 logging.debug("System load: %s", utils.run(['uptime']).stdout) 671 logging.info("Starting master ssh connection '%s'", master_cmd) 672 self.master_ssh_job = utils.BgJob(master_cmd, 673 nickname='master-ssh', 674 no_pipes=True) 675 # To prevent a race between the the master ssh connection startup 676 # and its first attempted use, wait for socket file to exist before 677 # returning. 678 end_time = time.time() + timeout 679 socket_file_path = os.path.join(self.master_ssh_tempdir.name, 680 'socket') 681 while time.time() < end_time: 682 if os.path.exists(socket_file_path): 683 break 684 time.sleep(.2) 685 else: 686 logging.warning('Timed out waiting for master-ssh connection ' 687 'to be established.') 688 689 690 def clear_known_hosts(self): 691 """Clears out the temporary ssh known_hosts file. 692 693 This is useful if the test SSHes to the machine, then reinstalls it, 694 then SSHes to it again. It can be called after the reinstall to 695 reduce the spam in the logs. 696 """ 697 logging.info("Clearing known hosts for host '%s', file '%s'.", 698 self.hostname, self.known_hosts_file) 699 # Clear out the file by opening it for writing and then closing. 700 fh = open(self.known_hosts_file, "w") 701 fh.close() 702 703 704 def collect_logs(self, remote_src_dir, local_dest_dir, ignore_errors=True): 705 """Copy log directories from a host to a local directory. 706 707 @param remote_src_dir: A destination directory on the host. 708 @param local_dest_dir: A path to a local destination directory. 709 If it doesn't exist it will be created. 710 @param ignore_errors: If True, ignore exceptions. 711 712 @raises OSError: If there were problems creating the local_dest_dir and 713 ignore_errors is False. 714 @raises AutoservRunError, AutotestRunError: If something goes wrong 715 while copying the directories and ignore_errors is False. 716 """ 717 locally_created_dest = False 718 if (not os.path.exists(local_dest_dir) 719 or not os.path.isdir(local_dest_dir)): 720 try: 721 os.makedirs(local_dest_dir) 722 locally_created_dest = True 723 except OSError as e: 724 logging.warning('Unable to collect logs from host ' 725 '%s: %s', self.hostname, e) 726 if not ignore_errors: 727 raise 728 return 729 try: 730 self.get_file( 731 remote_src_dir, local_dest_dir, preserve_symlinks=True) 732 except (error.AutotestRunError, error.AutoservRunError, 733 error.AutoservSSHTimeout) as e: 734 logging.warning('Collection of %s to local dir %s from host %s ' 735 'failed: %s', remote_src_dir, local_dest_dir, 736 self.hostname, e) 737 if locally_created_dest: 738 shutil.rmtree(local_dest_dir, ignore_errors=ignore_errors) 739 if not ignore_errors: 740 raise 741 742 743